diff --git a/cmd/zitadel/setup.yaml b/cmd/zitadel/setup.yaml index 3272ca011e..79611a5022 100644 --- a/cmd/zitadel/setup.yaml +++ b/cmd/zitadel/setup.yaml @@ -96,4 +96,6 @@ SetUp: PrimaryColor: '#222324' SecondaryColor: '#ffffff' Step7: - DefaultSecondFactor: 1 #SecondFactorTypeOTP \ No newline at end of file + DefaultSecondFactor: 1 #SecondFactorTypeOTP + Step8: + DefaultSecondFactor: 2 #SecondFactorTypeU2F \ No newline at end of file diff --git a/cmd/zitadel/system-defaults.yaml b/cmd/zitadel/system-defaults.yaml index f4cf9c2be2..5faee8baec 100644 --- a/cmd/zitadel/system-defaults.yaml +++ b/cmd/zitadel/system-defaults.yaml @@ -53,7 +53,7 @@ SystemDefaults: VerificationLifetimes: PasswordCheck: 240h #10d ExternalLoginCheck: 240h #10d - MfaInitSkip: 720h #30d + MFAInitSkip: 720h #30d SecondFactorCheck: 18h MultiFactorCheck: 12h IamID: 'IAM' @@ -124,4 +124,8 @@ SystemDefaults: Subject: 'DomainClaimed.Subject' Greeting: 'DomainClaimed.Greeting' Text: 'DomainClaimed.Text' - ButtonText: 'DomainClaimed.ButtonText' \ No newline at end of file + ButtonText: 'DomainClaimed.ButtonText' + WebAuthN: + ID: $ZITADEL_COOKIE_DOMAIN + Origin: $ZITADEL_ACCOUNTS + DisplayName: ZITADEL \ No newline at end of file diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index d505ea77e7..a003ec328b 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -164,8 +164,8 @@ "OTP_DIALOG_DESCRIPTION": "Scanne den QR-Code mit einer Authenticator App und verifiziere den erhaltenen Code, um OTP zu aktivieren.", "TYPE": { "0":"Keine MFA definiert", - "1":"SMS", - "2":"OTP" + "1":"OTP", + "2":"U2F" }, "STATE": { "0": "Kein Status", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 3072b341fd..b4f01ba663 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -164,8 +164,8 @@ "OTP_DIALOG_DESCRIPTION": "Scan the QR code with an authenticator app and enter the code below to verify and activate the OTP method.", "TYPE": { "0": "No MFA defined", - "1": "SMS", - "2": "OTP" + "1": "OTP", + "2": "U2F" }, "STATE": { "0": "No State", diff --git a/go.mod b/go.mod index 202a94667e..99b0b77d12 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/caos/logging v0.0.2 github.com/caos/oidc v0.13.1 github.com/cockroachdb/cockroach-go/v2 v2.0.8 + github.com/duo-labs/webauthn v0.0.0-20200714211715-1daaee874e43 github.com/envoyproxy/protoc-gen-validate v0.4.1 github.com/ghodss/yaml v1.0.0 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b diff --git a/go.sum b/go.sum index e6b4d8a17a..4a327f6ca0 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1CUzYdAZXijuvLuRMirgiXdf3zsM2Ig= +github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= @@ -152,7 +154,10 @@ github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9r github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/duo-labs/webauthn v0.0.0-20200714211715-1daaee874e43 h1:eEEfwrmEwl0LVuWz/VkAefdgtPbX174Huu5dxxceihI= +github.com/duo-labs/webauthn v0.0.0-20200714211715-1daaee874e43/go.mod h1:/X2OJiJxjQ7alqWZqX9EtBTmZc+4qQ0LvZ1k5wP67RM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= @@ -175,6 +180,8 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ= +github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= @@ -252,6 +259,8 @@ github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -492,6 +501,7 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -644,6 +654,8 @@ github.com/ttacon/libphonenumber v1.1.0 h1:tC6kE4t8UI4OqQVQjW5q8gSWhG2wnY5moEpSE github.com/ttacon/libphonenumber v1.1.0/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/admin/repository/eventsourcing/handler/user.go b/internal/admin/repository/eventsourcing/handler/user.go index f93c49c048..f0f26afc4f 100644 --- a/internal/admin/repository/eventsourcing/handler/user.go +++ b/internal/admin/repository/eventsourcing/handler/user.go @@ -92,6 +92,12 @@ func (u *User) ProcessUser(event *models.Event) (err error) { es_model.HumanMFAOTPAdded, es_model.HumanMFAOTPVerified, es_model.HumanMFAOTPRemoved, + es_model.HumanMFAU2FTokenAdded, + es_model.HumanMFAU2FTokenVerified, + es_model.HumanMFAU2FTokenRemoved, + es_model.HumanPasswordlessTokenAdded, + es_model.HumanPasswordlessTokenVerified, + es_model.HumanPasswordlessTokenRemoved, es_model.MachineChanged: user, err = u.view.UserByID(event.AggregateID) if err != nil { diff --git a/internal/admin/repository/eventsourcing/view/user.go b/internal/admin/repository/eventsourcing/view/user.go index 75fb8878e8..3de68472cf 100644 --- a/internal/admin/repository/eventsourcing/view/user.go +++ b/internal/admin/repository/eventsourcing/view/user.go @@ -36,8 +36,8 @@ func (v *View) IsUserUnique(userName, email string) (bool, error) { return view.IsUserUnique(v.Db, userTable, userName, email) } -func (v *View) UserMfas(userID string) ([]*usr_model.MultiFactor, error) { - return view.UserMfas(v.Db, userTable, userID) +func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) { + return view.UserMFAs(v.Db, userTable, userID) } func (v *View) PutUsers(user []*model.UserView, sequence uint64, eventTimestamp time.Time) error { diff --git a/internal/api/grpc/admin/login_policy_converter.go b/internal/api/grpc/admin/login_policy_converter.go index 90a5ccc415..d45af029d4 100644 --- a/internal/api/grpc/admin/login_policy_converter.go +++ b/internal/api/grpc/admin/login_policy_converter.go @@ -13,6 +13,7 @@ func loginPolicyToModel(policy *admin.DefaultLoginPolicyRequest) *iam_model.Logi AllowExternalIdp: policy.AllowExternalIdp, AllowRegister: policy.AllowRegister, ForceMFA: policy.ForceMfa, + PasswordlessType: passwordlessTypeToModel(policy.PasswordlessType), } } @@ -28,6 +29,7 @@ func loginPolicyFromModel(policy *iam_model.LoginPolicy) *admin.DefaultLoginPoli AllowExternalIdp: policy.AllowExternalIdp, AllowRegister: policy.AllowRegister, ForceMfa: policy.ForceMFA, + PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType), CreationDate: creationDate, ChangeDate: changeDate, } @@ -45,6 +47,7 @@ func loginPolicyViewFromModel(policy *iam_model.LoginPolicyView) *admin.DefaultL AllowExternalIdp: policy.AllowExternalIDP, AllowRegister: policy.AllowRegister, ForceMfa: policy.ForceMFA, + PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType), CreationDate: creationDate, ChangeDate: changeDate, } @@ -145,6 +148,24 @@ func secondFactorTypeToModel(mfaType *admin.SecondFactor) iam_model.SecondFactor } } +func passwordlessTypeFromModel(passwordlessType iam_model.PasswordlessType) admin.PasswordlessType { + switch passwordlessType { + case iam_model.PasswordlessTypeAllowed: + return admin.PasswordlessType_PASSWORDLESSTYPE_ALLOWED + default: + return admin.PasswordlessType_PASSWORDLESSTYPE_NOT_ALLOWED + } +} + +func passwordlessTypeToModel(passwordlessType admin.PasswordlessType) iam_model.PasswordlessType { + switch passwordlessType { + case admin.PasswordlessType_PASSWORDLESSTYPE_ALLOWED: + return iam_model.PasswordlessTypeAllowed + default: + return iam_model.PasswordlessTypeNotAllowed + } +} + func multiFactorResultFromModel(result *iam_model.MultiFactorsSearchResponse) *admin.MultiFactorsResult { converted := make([]admin.MultiFactorType, len(result.Result)) for i, mfaType := range result.Result { diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index ad0dba523f..e2d94e9d47 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -2,7 +2,6 @@ package auth import ( "context" - "github.com/golang/protobuf/ptypes/empty" "github.com/caos/zitadel/pkg/grpc/auth" @@ -54,7 +53,7 @@ func (s *Server) GetMyUserAddress(ctx context.Context, _ *empty.Empty) (*auth.Us } func (s *Server) GetMyMfas(ctx context.Context, _ *empty.Empty) (*auth.MultiFactors, error) { - mfas, err := s.repo.MyUserMfas(ctx) + mfas, err := s.repo.MyUserMFAs(ctx) if err != nil { return nil, err } @@ -144,7 +143,7 @@ func (s *Server) GetMyPasswordComplexityPolicy(ctx context.Context, _ *empty.Emp } func (s *Server) AddMfaOTP(ctx context.Context, _ *empty.Empty) (_ *auth.MfaOtpResponse, err error) { - otp, err := s.repo.AddMyMfaOTP(ctx) + otp, err := s.repo.AddMyMFAOTP(ctx) if err != nil { return nil, err } @@ -152,12 +151,42 @@ func (s *Server) AddMfaOTP(ctx context.Context, _ *empty.Empty) (_ *auth.MfaOtpR } func (s *Server) VerifyMfaOTP(ctx context.Context, request *auth.VerifyMfaOtp) (*empty.Empty, error) { - err := s.repo.VerifyMyMfaOTPSetup(ctx, request.Code) + err := s.repo.VerifyMyMFAOTPSetup(ctx, request.Code) return &empty.Empty{}, err } func (s *Server) RemoveMfaOTP(ctx context.Context, _ *empty.Empty) (_ *empty.Empty, err error) { - s.repo.RemoveMyMfaOTP(ctx) + err = s.repo.RemoveMyMFAOTP(ctx) + return &empty.Empty{}, err +} + +func (s *Server) AddMyMfaU2F(ctx context.Context, _ *empty.Empty) (_ *auth.WebAuthNResponse, err error) { + u2f, err := s.repo.AddMyMFAU2F(ctx) + return verifyWebAuthNFromModel(u2f), err +} + +func (s *Server) VerifyMyMfaU2F(ctx context.Context, request *auth.VerifyWebAuthN) (*empty.Empty, error) { + err := s.repo.VerifyMyMFAU2FSetup(ctx, request.TokenName, request.PublicKeyCredential) + return &empty.Empty{}, err +} + +func (s *Server) RemoveMyMfaU2F(ctx context.Context, id *auth.WebAuthNTokenID) (*empty.Empty, error) { + err := s.repo.RemoveMyMFAU2F(ctx, id.Id) + return &empty.Empty{}, err +} + +func (s *Server) AddMyPasswordless(ctx context.Context, _ *empty.Empty) (_ *auth.WebAuthNResponse, err error) { + u2f, err := s.repo.AddMyPasswordless(ctx) + return verifyWebAuthNFromModel(u2f), err +} + +func (s *Server) VerifyMyPasswordless(ctx context.Context, request *auth.VerifyWebAuthN) (*empty.Empty, error) { + err := s.repo.VerifyMyPasswordlessSetup(ctx, request.TokenName, request.PublicKeyCredential) + return &empty.Empty{}, err +} + +func (s *Server) RemoveMyPasswordless(ctx context.Context, id *auth.WebAuthNTokenID) (*empty.Empty, error) { + err := s.repo.RemoveMyPasswordless(ctx, id.Id) return &empty.Empty{}, err } diff --git a/internal/api/grpc/auth/user_converter.go b/internal/api/grpc/auth/user_converter.go index c6866ecf23..59b9daa844 100644 --- a/internal/api/grpc/auth/user_converter.go +++ b/internal/api/grpc/auth/user_converter.go @@ -358,11 +358,11 @@ func genderToModel(gender auth.Gender) usr_model.Gender { } } -func mfaStateFromModel(state usr_model.MfaState) auth.MFAState { +func mfaStateFromModel(state usr_model.MFAState) auth.MFAState { switch state { - case usr_model.MfaStateReady: + case usr_model.MFAStateReady: return auth.MFAState_MFASTATE_READY - case usr_model.MfaStateNotReady: + case usr_model.MFAStateNotReady: return auth.MFAState_MFASTATE_NOT_READY default: return auth.MFAState_MFASTATE_UNSPECIFIED @@ -379,17 +379,18 @@ func mfasFromModel(mfas []*usr_model.MultiFactor) []*auth.MultiFactor { func mfaFromModel(mfa *usr_model.MultiFactor) *auth.MultiFactor { return &auth.MultiFactor{ - State: mfaStateFromModel(mfa.State), - Type: mfaTypeFromModel(mfa.Type), + State: mfaStateFromModel(mfa.State), + Type: mfaTypeFromModel(mfa.Type), + Attribute: mfa.Attribute, } } -func mfaTypeFromModel(mfatype usr_model.MfaType) auth.MfaType { - switch mfatype { - case usr_model.MfaTypeOTP: +func mfaTypeFromModel(mfaType usr_model.MFAType) auth.MfaType { + switch mfaType { + case usr_model.MFATypeOTP: return auth.MfaType_MFATYPE_OTP - case usr_model.MfaTypeSMS: - return auth.MfaType_MFATYPE_SMS + case usr_model.MFATypeU2F: + return auth.MfaType_MFATYPE_U2F default: return auth.MfaType_MFATYPE_UNSPECIFIED } @@ -426,3 +427,11 @@ func userChangesToAPI(changes *usr_model.UserChanges) (_ []*auth.Change) { return result } + +func verifyWebAuthNFromModel(u2f *usr_model.WebAuthNToken) *auth.WebAuthNResponse { + return &auth.WebAuthNResponse{ + Id: u2f.WebAuthNTokenID, + PublicKey: u2f.PublicKey, + State: mfaStateFromModel(u2f.State), + } +} diff --git a/internal/api/grpc/management/login_policy_converter.go b/internal/api/grpc/management/login_policy_converter.go index f43691e247..6399233a98 100644 --- a/internal/api/grpc/management/login_policy_converter.go +++ b/internal/api/grpc/management/login_policy_converter.go @@ -13,6 +13,7 @@ func loginPolicyRequestToModel(policy *management.LoginPolicyRequest) *iam_model AllowExternalIdp: policy.AllowExternalIdp, AllowRegister: policy.AllowRegister, ForceMFA: policy.ForceMfa, + PasswordlessType: passwordlessTypeToModel(policy.PasswordlessType), } } @@ -30,6 +31,7 @@ func loginPolicyFromModel(policy *iam_model.LoginPolicy) *management.LoginPolicy CreationDate: creationDate, ChangeDate: changeDate, ForceMfa: policy.ForceMFA, + PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType), } } @@ -48,6 +50,7 @@ func loginPolicyViewFromModel(policy *iam_model.LoginPolicyView) *management.Log CreationDate: creationDate, ChangeDate: changeDate, ForceMfa: policy.ForceMFA, + PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType), } } @@ -215,3 +218,21 @@ func multiFactorTypeToModel(mfaType *management.MultiFactor) iam_model.MultiFact return iam_model.MultiFactorTypeUnspecified } } + +func passwordlessTypeFromModel(passwordlessType iam_model.PasswordlessType) management.PasswordlessType { + switch passwordlessType { + case iam_model.PasswordlessTypeAllowed: + return management.PasswordlessType_PASSWORDLESSTYPE_ALLOWED + default: + return management.PasswordlessType_PASSWORDLESSTYPE_NOT_ALLOWED + } +} + +func passwordlessTypeToModel(passwordlessType management.PasswordlessType) iam_model.PasswordlessType { + switch passwordlessType { + case management.PasswordlessType_PASSWORDLESSTYPE_ALLOWED: + return iam_model.PasswordlessTypeAllowed + default: + return iam_model.PasswordlessTypeNotAllowed + } +} diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index bf7f0abbd8..031503118c 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -214,7 +214,7 @@ func (s *Server) RemoveExternalIDP(ctx context.Context, request *management.Exte } func (s *Server) GetUserMfas(ctx context.Context, userID *management.UserID) (*management.UserMultiFactors, error) { - mfas, err := s.user.UserMfas(ctx, userID.Id) + mfas, err := s.user.UserMFAs(ctx, userID.Id) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index c1e38301d7..5f10e61a72 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -572,22 +572,22 @@ func genderToModel(gender management.Gender) usr_model.Gender { } } -func mfaTypeFromModel(mfatype usr_model.MfaType) management.MfaType { +func mfaTypeFromModel(mfatype usr_model.MFAType) management.MfaType { switch mfatype { - case usr_model.MfaTypeOTP: + case usr_model.MFATypeOTP: return management.MfaType_MFATYPE_OTP - case usr_model.MfaTypeSMS: - return management.MfaType_MFATYPE_SMS + case usr_model.MFATypeU2F: + return management.MfaType_MFATYPE_U2F default: return management.MfaType_MFATYPE_UNSPECIFIED } } -func mfaStateFromModel(state usr_model.MfaState) management.MFAState { +func mfaStateFromModel(state usr_model.MFAState) management.MFAState { switch state { - case usr_model.MfaStateReady: + case usr_model.MFAStateReady: return management.MFAState_MFASTATE_READY - case usr_model.MfaStateNotReady: + case usr_model.MFAStateNotReady: return management.MFAState_MFASTATE_NOT_READY default: return management.MFAState_MFASTATE_UNSPECIFIED diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 06dba25dfe..5dd4dc15b6 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -15,9 +15,10 @@ import ( ) const ( - amrPassword = "password" - amrMFA = "mfa" - amrOTP = "otp" + amrPassword = "password" + amrMFA = "mfa" + amrOTP = "otp" + amrUserPresence = "user" ) type AuthRequest struct { @@ -38,11 +39,11 @@ func (a *AuthRequest) GetAMR() []string { if a.PasswordVerified { amr = append(amr, amrPassword) } - if len(a.MfasVerified) > 0 { + if len(a.MFAsVerified) > 0 { amr = append(amr, amrMFA) - for _, mfa := range a.MfasVerified { - if amrMfa := AMRFromMFAType(mfa); amrMfa != "" { - amr = append(amr, amrMfa) + for _, mfa := range a.MFAsVerified { + if amrMFA := AMRFromMFAType(mfa); amrMFA != "" { + amr = append(amr, amrMFA) } } } @@ -247,6 +248,9 @@ func AMRFromMFAType(mfaType model.MFAType) string { switch mfaType { case model.MFATypeOTP: return amrOTP + case model.MFATypeU2F, + model.MFATypeU2FUserVerification: + return amrUserPresence default: return "" } diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index c3301296a6..3aaab44596 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -2,10 +2,9 @@ package repository import ( "context" + "github.com/caos/zitadel/internal/auth_request/model" org_model "github.com/caos/zitadel/internal/org/model" user_model "github.com/caos/zitadel/internal/user/model" - - "github.com/caos/zitadel/internal/auth_request/model" ) type AuthRequestRepository interface { @@ -15,14 +14,22 @@ type AuthRequestRepository interface { AuthRequestByCode(ctx context.Context, code string) (*model.AuthRequest, error) SaveAuthCode(ctx context.Context, id, code, userAgentID string) error DeleteAuthRequest(ctx context.Context, id string) error + CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *model.ExternalUser, info *model.BrowserInfo) error SelectUser(ctx context.Context, id, userID, userAgentID string) error SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error VerifyPassword(ctx context.Context, id, userID, password, userAgentID string, info *model.BrowserInfo) error - VerifyMfaOTP(ctx context.Context, agentID, authRequestID, code, userAgentID string, info *model.BrowserInfo) error + + VerifyMFAOTP(ctx context.Context, agentID, authRequestID, code, userAgentID string, info *model.BrowserInfo) error + BeginMFAU2FLogin(ctx context.Context, userID, authRequestID, userAgentID string) (*user_model.WebAuthNLogin, error) + VerifyMFAU2F(ctx context.Context, userID, authRequestID, userAgentID string, credentialData []byte, info *model.BrowserInfo) error + BeginPasswordlessLogin(ctx context.Context, userID, authRequestID, userAgentID string) (*user_model.WebAuthNLogin, error) + VerifyPasswordless(ctx context.Context, userID, authRequestID, userAgentID string, credentialData []byte, info *model.BrowserInfo) error + LinkExternalUsers(ctx context.Context, authReqID, userAgentID string, info *model.BrowserInfo) error AutoRegisterExternalUser(ctx context.Context, user *user_model.User, externalIDP *user_model.ExternalIDP, member *org_model.OrgMember, authReqID, userAgentID, resourceOwner string, info *model.BrowserInfo) error ResetLinkingUsers(ctx context.Context, authReqID, userAgentID string) error + GetOrgByPrimaryDomain(primaryDomain string) (*org_model.OrgView, error) } diff --git a/internal/auth/repository/eventsourcing/eventstore/application.go b/internal/auth/repository/eventsourcing/eventstore/application.go index 5d5bc76102..95540846fb 100644 --- a/internal/auth/repository/eventsourcing/eventstore/application.go +++ b/internal/auth/repository/eventsourcing/eventstore/application.go @@ -7,6 +7,7 @@ import ( "github.com/caos/zitadel/internal/project/model" proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing" proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model" + "github.com/caos/zitadel/internal/telemetry/tracing" ) type ApplicationRepo struct { @@ -22,7 +23,10 @@ func (a *ApplicationRepo) ApplicationByClientID(ctx context.Context, clientID st return proj_view_model.ApplicationViewToModel(app), nil } -func (a *ApplicationRepo) AuthorizeOIDCApplication(ctx context.Context, clientID, secret string) error { +func (a *ApplicationRepo) AuthorizeOIDCApplication(ctx context.Context, clientID, secret string) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + app, err := a.View.ApplicationByClientID(ctx, clientID) if err != nil { return err diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index f243000410..7c7a7bb8ea 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -47,7 +47,7 @@ type AuthRequestRepo struct { PasswordCheckLifeTime time.Duration ExternalLoginCheckLifeTime time.Duration - MfaInitSkippedLifeTime time.Duration + MFAInitSkippedLifeTime time.Duration SecondFactorCheckLifeTime time.Duration MultiFactorCheckLifeTime time.Duration @@ -245,27 +245,62 @@ func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID, userAge func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, password, userAgentID string, info *model.BrowserInfo) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - request, err := repo.getAuthRequest(ctx, id, userAgentID) + request, err := repo.getAuthRequestEnsureUser(ctx, id, userAgentID, userID) if err != nil { return err } - if request.UserID != userID { - return errors.ThrowPreconditionFailed(nil, "EVENT-ds35D", "Errors.User.NotMatchingUserID") - } return repo.UserEvents.CheckPassword(ctx, userID, password, request.WithCurrentInfo(info)) } -func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, userID, code, userAgentID string, info *model.BrowserInfo) (err error) { +func (repo *AuthRequestRepo) VerifyMFAOTP(ctx context.Context, authRequestID, userID, code, userAgentID string, info *model.BrowserInfo) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - request, err := repo.getAuthRequest(ctx, authRequestID, userAgentID) + request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID) if err != nil { return err } - if request.UserID != userID { - return errors.ThrowPreconditionFailed(nil, "EVENT-ADJ26", "Errors.User.NotMatchingUserID") + return repo.UserEvents.CheckMFAOTP(ctx, userID, code, request.WithCurrentInfo(info)) +} + +func (repo *AuthRequestRepo) BeginMFAU2FLogin(ctx context.Context, userID, authRequestID, userAgentID string) (login *user_model.WebAuthNLogin, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID) + if err != nil { + return nil, err } - return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info)) + return repo.UserEvents.BeginU2FLogin(ctx, userID, request) +} + +func (repo *AuthRequestRepo) VerifyMFAU2F(ctx context.Context, userID, authRequestID, userAgentID string, credentialData []byte, info *model.BrowserInfo) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID) + if err != nil { + return err + } + return repo.UserEvents.VerifyMFAU2F(ctx, userID, credentialData, request) +} + +func (repo *AuthRequestRepo) BeginPasswordlessLogin(ctx context.Context, userID, authRequestID, userAgentID string) (login *user_model.WebAuthNLogin, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID) + if err != nil { + return nil, err + } + return repo.UserEvents.BeginPasswordlessLogin(ctx, userID, request) +} + +func (repo *AuthRequestRepo) VerifyPasswordless(ctx context.Context, userID, authRequestID, userAgentID string, credentialData []byte, info *model.BrowserInfo) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID) + if err != nil { + return err + } + return repo.UserEvents.VerifyPasswordless(ctx, userID, credentialData, request) } func (repo *AuthRequestRepo) LinkExternalUsers(ctx context.Context, authReqID, userAgentID string, info *model.BrowserInfo) (err error) { @@ -365,6 +400,17 @@ func (repo *AuthRequestRepo) getAuthRequestNextSteps(ctx context.Context, id, us return request, nil } +func (repo *AuthRequestRepo) getAuthRequestEnsureUser(ctx context.Context, authRequestID, userAgentID, userID string) (*model.AuthRequest, error) { + request, err := repo.getAuthRequest(ctx, authRequestID, userAgentID) + if err != nil { + return nil, err + } + if request.UserID != userID { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-GBH32", "Errors.User.NotMatchingUserID") + } + return request, nil +} + func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id, userAgentID string) (*model.AuthRequest, error) { request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) if err != nil { @@ -545,27 +591,19 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR return nil, err } - if (request.SelectedIDPConfigID != "" || userSession.SelectedIDPConfigID != "") && (request.LinkingUsers == nil || len(request.LinkingUsers) == 0) { - if !checkVerificationTime(userSession.ExternalLoginVerification, repo.ExternalLoginCheckLifeTime) { - selectedIDPConfigID := request.SelectedIDPConfigID - if selectedIDPConfigID == "" { - selectedIDPConfigID = userSession.SelectedIDPConfigID - } - return append(steps, &model.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID}), nil + isInternalLogin := request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == "" + if !isInternalLogin && len(request.LinkingUsers) == 0 && !checkVerificationTime(userSession.ExternalLoginVerification, repo.ExternalLoginCheckLifeTime) { + selectedIDPConfigID := request.SelectedIDPConfigID + if selectedIDPConfigID == "" { + selectedIDPConfigID = userSession.SelectedIDPConfigID } - } else if (request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == "") || (request.SelectedIDPConfigID != "" && request.LinkingUsers != nil && len(request.LinkingUsers) > 0) { - if user.InitRequired { - return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil + return append(steps, &model.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID}), nil + } + if isInternalLogin || (!isInternalLogin && len(request.LinkingUsers) > 0) { + step := repo.firstFactorChecked(request, user, userSession) + if step != nil { + return append(steps, step), nil } - if !user.PasswordSet { - return append(steps, &model.InitPasswordStep{}), nil - } - - if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) { - return append(steps, &model.PasswordStep{}), nil - } - request.PasswordVerified = true - request.AuthTime = userSession.PasswordVerification } step, ok, err := repo.mfaChecked(userSession, request, user) @@ -624,21 +662,46 @@ func (repo *AuthRequestRepo) usersForUserSelection(request *model.AuthRequest) ( return users, nil } +func (repo *AuthRequestRepo) firstFactorChecked(request *model.AuthRequest, user *user_model.UserView, userSession *user_model.UserSessionView) model.NextStep { + if user.InitRequired { + return &model.InitUserStep{PasswordSet: user.PasswordSet} + } + + if user.IsPasswordlessReady() { + if !checkVerificationTime(userSession.PasswordlessVerification, repo.MultiFactorCheckLifeTime) { + return &model.PasswordlessStep{} + } + request.AuthTime = userSession.PasswordlessVerification + return nil + } + + if !user.PasswordSet { + return &model.InitPasswordStep{} + } + + if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) { + return &model.PasswordStep{} + } + request.PasswordVerified = true + request.AuthTime = userSession.PasswordVerification + return nil +} + func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *model.AuthRequest, user *user_model.UserView) (model.NextStep, bool, error) { - mfaLevel := request.MfaLevel() - allowedProviders, required := user.MfaTypesAllowed(mfaLevel, request.LoginPolicy) - promptRequired := (user.MfaMaxSetUp < mfaLevel) || (len(allowedProviders) == 0 && required) + mfaLevel := request.MFALevel() + allowedProviders, required := user.MFATypesAllowed(mfaLevel, request.LoginPolicy) + promptRequired := (user.MFAMaxSetUp < mfaLevel) || (len(allowedProviders) == 0 && required) if promptRequired || !repo.mfaSkippedOrSetUp(user) { - types := user.MfaTypesSetupPossible(mfaLevel, request.LoginPolicy) + types := user.MFATypesSetupPossible(mfaLevel, request.LoginPolicy) if promptRequired && len(types) == 0 { return nil, false, errors.ThrowPreconditionFailed(nil, "LOGIN-5Hm8s", "Errors.Login.LoginPolicy.MFA.ForceAndNotConfigured") } if len(types) == 0 { return nil, true, nil } - return &model.MfaPromptStep{ + return &model.MFAPromptStep{ Required: promptRequired, - MfaProviders: types, + MFAProviders: types, }, false, nil } switch mfaLevel { @@ -651,28 +714,28 @@ func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, fallthrough case model.MFALevelSecondFactor: if checkVerificationTime(userSession.SecondFactorVerification, repo.SecondFactorCheckLifeTime) { - request.MfasVerified = append(request.MfasVerified, userSession.SecondFactorVerificationType) + request.MFAsVerified = append(request.MFAsVerified, userSession.SecondFactorVerificationType) request.AuthTime = userSession.SecondFactorVerification return nil, true, nil } fallthrough case model.MFALevelMultiFactor: if checkVerificationTime(userSession.MultiFactorVerification, repo.MultiFactorCheckLifeTime) { - request.MfasVerified = append(request.MfasVerified, userSession.MultiFactorVerificationType) + request.MFAsVerified = append(request.MFAsVerified, userSession.MultiFactorVerificationType) request.AuthTime = userSession.MultiFactorVerification return nil, true, nil } } - return &model.MfaVerificationStep{ - MfaProviders: allowedProviders, + return &model.MFAVerificationStep{ + MFAProviders: allowedProviders, }, false, nil } func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool { - if user.MfaMaxSetUp > model.MFALevelNotSetUp { + if user.MFAMaxSetUp > model.MFALevelNotSetUp { return true } - return checkVerificationTime(user.MfaInitSkipped, repo.MfaInitSkippedLifeTime) + return checkVerificationTime(user.MFAInitSkipped, repo.MFAInitSkippedLifeTime) } func (repo *AuthRequestRepo) getLoginPolicy(ctx context.Context, orgID string) (*iam_model.LoginPolicyView, error) { @@ -745,7 +808,11 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve es_model.HumanExternalLoginCheckSucceeded, es_model.HumanMFAOTPCheckSucceeded, es_model.HumanMFAOTPCheckFailed, - es_model.HumanSignedOut: + es_model.HumanSignedOut, + es_model.HumanPasswordlessTokenCheckSucceeded, + es_model.HumanPasswordlessTokenCheckFailed, + es_model.HumanMFAU2FTokenCheckSucceeded, + es_model.HumanMFAU2FTokenCheckFailed: eventData, err := user_view_model.UserSessionFromEvent(event) if err != nil { logging.Log("EVENT-sdgT3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error getting event data") diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index e12ea165e5..345bbfc727 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -48,8 +48,10 @@ func (m *mockViewErrUserSession) UserSessionsByAgentID(string) ([]*user_view_mod type mockViewUserSession struct { ExternalLoginVerification time.Time + PasswordlessVerification time.Time PasswordVerification time.Time SecondFactorVerification time.Time + MultiFactorVerification time.Time Users []mockUser } @@ -61,8 +63,10 @@ type mockUser struct { func (m *mockViewUserSession) UserSessionByIDs(string, string) (*user_view_model.UserSessionView, error) { return &user_view_model.UserSessionView{ ExternalLoginVerification: m.ExternalLoginVerification, + PasswordlessVerification: m.PasswordlessVerification, PasswordVerification: m.PasswordVerification, SecondFactorVerification: m.SecondFactorVerification, + MultiFactorVerification: m.MultiFactorVerification, }, nil } @@ -115,8 +119,9 @@ type mockViewUser struct { PasswordChangeRequired bool IsEmailVerified bool OTPState int32 - MfaMaxSetUp int32 - MfaInitSkipped time.Time + MFAMaxSetUp int32 + MFAInitSkipped time.Time + PasswordlessTokens user_view_model.WebAuthNTokens } type mockLoginPolicy struct { @@ -138,8 +143,9 @@ func (m *mockViewUser) UserByID(string) (*user_view_model.UserView, error) { PasswordChangeRequired: m.PasswordChangeRequired, IsEmailVerified: m.IsEmailVerified, OTPState: m.OTPState, - MfaMaxSetUp: m.MfaMaxSetUp, - MfaInitSkipped: m.MfaInitSkipped, + MFAMaxSetUp: m.MFAMaxSetUp, + MFAInitSkipped: m.MFAInitSkipped, + PasswordlessTokens: m.PasswordlessTokens, }, }, nil } @@ -200,7 +206,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { loginPolicyProvider loginPolicyViewProvider PasswordCheckLifeTime time.Duration ExternalLoginCheckLifeTime time.Duration - MfaInitSkippedLifeTime time.Duration + MFAInitSkippedLifeTime time.Duration SecondFactorCheckLifeTime time.Duration MultiFactorCheckLifeTime time.Duration } @@ -413,6 +419,49 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }}, nil, }, + { + "passwordless not verified, passwordless check step", + fields{ + userSessionViewProvider: &mockViewUserSession{}, + userViewProvider: &mockViewUser{ + PasswordSet: true, + PasswordlessTokens: user_view_model.WebAuthNTokens{&user_view_model.WebAuthNView{ID: "id", State: int32(user_model.MFAStateReady)}}, + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + MultiFactorCheckLifeTime: 10 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID"}, false}, + []model.NextStep{&model.PasswordlessStep{}}, + nil, + }, + { + "passwordless verified, email not verified, email verification step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordlessVerification: time.Now().Add(-5 * time.Minute), + MultiFactorVerification: time.Now().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + PasswordlessTokens: user_view_model.WebAuthNTokens{&user_view_model.WebAuthNView{ID: "id", State: int32(user_model.MFAStateReady)}}, + PasswordChangeRequired: false, + IsEmailVerified: false, + MFAMaxSetUp: int32(model.MFALevelMultiFactor), + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + MultiFactorCheckLifeTime: 10 * time.Hour, + }, + args{&model.AuthRequest{ + UserID: "UserID", + LoginPolicy: &iam_model.LoginPolicyView{ + MultiFactors: []iam_model.MultiFactorType{iam_model.MultiFactorTypeU2FWithPIN}, + }, + }, false}, + []model.NextStep{&model.VerifyEMailStep{}}, + nil, + }, { "password not set, init password step", fields{ @@ -433,7 +482,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, userViewProvider: &mockViewUser{ IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -452,7 +501,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, userViewProvider: &mockViewUser{ IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -499,7 +548,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -525,8 +574,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, userViewProvider: &mockViewUser{ PasswordSet: true, - OTPState: int32(user_model.MfaStateReady), - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + OTPState: int32(user_model.MFAStateReady), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -540,8 +589,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { SecondFactors: []iam_model.SecondFactorType{iam_model.SecondFactorTypeOTP}, }, }, false}, - []model.NextStep{&model.MfaVerificationStep{ - MfaProviders: []model.MFAType{model.MFATypeOTP}, + []model.NextStep{&model.MFAVerificationStep{ + MFAProviders: []model.MFAType{model.MFATypeOTP}, }}, nil, }, @@ -554,8 +603,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, userViewProvider: &mockViewUser{ PasswordSet: true, - OTPState: int32(user_model.MfaStateReady), - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + OTPState: int32(user_model.MFAStateReady), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -571,8 +620,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { SecondFactors: []iam_model.SecondFactorType{iam_model.SecondFactorTypeOTP}, }, }, false}, - []model.NextStep{&model.MfaVerificationStep{ - MfaProviders: []model.MFAType{model.MFATypeOTP}, + []model.NextStep{&model.MFAVerificationStep{ + MFAProviders: []model.MFAType{model.MFATypeOTP}, }}, nil, }, @@ -587,7 +636,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { PasswordSet: true, PasswordChangeRequired: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -613,7 +662,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, userViewProvider: &mockViewUser{ PasswordSet: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -639,7 +688,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, PasswordChangeRequired: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -665,7 +714,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -693,7 +742,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -722,7 +771,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -754,7 +803,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -785,7 +834,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -810,7 +859,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, - MfaMaxSetUp: int32(model.MFALevelSecondFactor), + MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, userEventProvider: &mockEventUser{}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, @@ -844,7 +893,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { LoginPolicyViewProvider: tt.fields.loginPolicyProvider, PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime, ExternalLoginCheckLifeTime: tt.fields.ExternalLoginCheckLifeTime, - MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, + MFAInitSkippedLifeTime: tt.fields.MFAInitSkippedLifeTime, SecondFactorCheckLifeTime: tt.fields.SecondFactorCheckLifeTime, MultiFactorCheckLifeTime: tt.fields.MultiFactorCheckLifeTime, } @@ -860,7 +909,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { func TestAuthRequestRepo_mfaChecked(t *testing.T) { type fields struct { - MfaInitSkippedLifeTime time.Duration + MFAInitSkippedLifeTime time.Duration SecondFactorCheckLifeTime time.Duration MultiFactorCheckLifeTime time.Duration } @@ -884,7 +933,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { // args{ // request: &model.AuthRequest{PossibleLOAs: []model.LevelOfAssurance{}}, // user: &user_model.UserView{ - // OTPState: user_model.MfaStateReady, + // OTPState: user_model.MFAStateReady, // }, // }, // false, @@ -892,7 +941,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { { "not set up, forced by policy, no mfas configured, error", fields{ - MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + MFAInitSkippedLifeTime: 30 * 24 * time.Hour, }, args{ request: &model.AuthRequest{ @@ -902,7 +951,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { }, user: &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelNotSetUp, + MFAMaxSetUp: model.MFALevelNotSetUp, }, }, }, @@ -913,7 +962,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { { "not set up, no mfas configured, no prompt and true", fields{ - MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + MFAInitSkippedLifeTime: 30 * 24 * time.Hour, }, args{ request: &model.AuthRequest{ @@ -921,7 +970,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { }, user: &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelNotSetUp, + MFAMaxSetUp: model.MFALevelNotSetUp, }, }, }, @@ -932,7 +981,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { { "not set up, prompt and false", fields{ - MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + MFAInitSkippedLifeTime: 30 * 24 * time.Hour, }, args{ request: &model.AuthRequest{ @@ -942,12 +991,12 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { }, user: &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelNotSetUp, + MFAMaxSetUp: model.MFALevelNotSetUp, }, }, }, - &model.MfaPromptStep{ - MfaProviders: []model.MFAType{ + &model.MFAPromptStep{ + MFAProviders: []model.MFAType{ model.MFATypeOTP, }, }, @@ -957,7 +1006,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { { "not set up, forced by org, true", fields{ - MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + MFAInitSkippedLifeTime: 30 * 24 * time.Hour, }, args{ request: &model.AuthRequest{ @@ -968,13 +1017,13 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { }, user: &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelNotSetUp, + MFAMaxSetUp: model.MFALevelNotSetUp, }, }, }, - &model.MfaPromptStep{ + &model.MFAPromptStep{ Required: true, - MfaProviders: []model.MFAType{ + MFAProviders: []model.MFAType{ model.MFATypeOTP, }, }, @@ -984,7 +1033,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { { "not set up and skipped, true", fields{ - MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + MFAInitSkippedLifeTime: 30 * 24 * time.Hour, }, args{ request: &model.AuthRequest{ @@ -992,8 +1041,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { }, user: &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelNotSetUp, - MfaInitSkipped: time.Now().UTC(), + MFAMaxSetUp: model.MFALevelNotSetUp, + MFAInitSkipped: time.Now().UTC(), }, }, }, @@ -1014,8 +1063,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { }, user: &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelSecondFactor, - OTPState: user_model.MfaStateReady, + MFAMaxSetUp: model.MFALevelSecondFactor, + OTPState: user_model.MFAStateReady, }, }, userSession: &user_model.UserSessionView{SecondFactorVerification: time.Now().UTC().Add(-5 * time.Hour)}, @@ -1037,15 +1086,15 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { }, user: &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelSecondFactor, - OTPState: user_model.MfaStateReady, + MFAMaxSetUp: model.MFALevelSecondFactor, + OTPState: user_model.MFAStateReady, }, }, userSession: &user_model.UserSessionView{}, }, - &model.MfaVerificationStep{ - MfaProviders: []model.MFAType{model.MFATypeOTP}, + &model.MFAVerificationStep{ + MFAProviders: []model.MFAType{model.MFATypeOTP}, }, false, nil, @@ -1054,7 +1103,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &AuthRequestRepo{ - MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, + MFAInitSkippedLifeTime: tt.fields.MFAInitSkippedLifeTime, SecondFactorCheckLifeTime: tt.fields.SecondFactorCheckLifeTime, MultiFactorCheckLifeTime: tt.fields.MultiFactorCheckLifeTime, } @@ -1073,7 +1122,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) { func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) { type fields struct { - MfaInitSkippedLifeTime time.Duration + MFAInitSkippedLifeTime time.Duration } type args struct { user *user_model.UserView @@ -1090,7 +1139,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) { args{ &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: model.MFALevelSecondFactor, + MFAMaxSetUp: model.MFALevelSecondFactor, }, }, }, @@ -1099,13 +1148,13 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) { { "mfa skipped active, true", fields{ - MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + MFAInitSkippedLifeTime: 30 * 24 * time.Hour, }, args{ &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: -1, - MfaInitSkipped: time.Now().UTC().Add(-10 * time.Hour), + MFAMaxSetUp: -1, + MFAInitSkipped: time.Now().UTC().Add(-10 * time.Hour), }, }, }, @@ -1114,13 +1163,13 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) { { "mfa skipped inactive, false", fields{ - MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + MFAInitSkippedLifeTime: 30 * 24 * time.Hour, }, args{ &user_model.UserView{ HumanView: &user_model.HumanView{ - MfaMaxSetUp: -1, - MfaInitSkipped: time.Now().UTC().Add(-40 * 24 * time.Hour), + MFAMaxSetUp: -1, + MFAInitSkipped: time.Now().UTC().Add(-40 * 24 * time.Hour), }, }, }, @@ -1130,7 +1179,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &AuthRequestRepo{ - MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, + MFAInitSkippedLifeTime: tt.fields.MFAInitSkippedLifeTime, } if got := repo.mfaSkippedOrSetUp(tt.args.user); got != tt.want { t.Errorf("mfaSkippedOrSetUp() = %v, want %v", got, tt.want) diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index e5b473c85d..af6d62b2b1 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -253,18 +253,22 @@ func (repo *UserRepo) ChangePassword(ctx context.Context, userID, old, new strin return err } -func (repo *UserRepo) MyUserMfas(ctx context.Context) ([]*model.MultiFactor, error) { +func (repo *UserRepo) MyUserMFAs(ctx context.Context) ([]*model.MultiFactor, error) { user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) if err != nil { return nil, err } - if user.OTPState == model.MfaStateUnspecified { - return []*model.MultiFactor{}, nil + mfas := make([]*model.MultiFactor, 0) + if user.OTPState != model.MFAStateUnspecified { + mfas = append(mfas, &model.MultiFactor{Type: model.MFATypeOTP, State: user.OTPState}) } - return []*model.MultiFactor{{Type: model.MfaTypeOTP, State: user.OTPState}}, nil + for _, u2f := range user.U2FTokens { + mfas = append(mfas, &model.MultiFactor{Type: model.MFATypeU2F, State: u2f.State, Attribute: u2f.Name}) + } + return mfas, nil } -func (repo *UserRepo) AddMfaOTP(ctx context.Context, userID string) (*model.OTP, error) { +func (repo *UserRepo) AddMFAOTP(ctx context.Context, userID string) (*model.OTP, error) { accountName := "" user, err := repo.UserByID(ctx, userID) if err != nil { @@ -275,7 +279,7 @@ func (repo *UserRepo) AddMfaOTP(ctx context.Context, userID string) (*model.OTP, return repo.UserEvents.AddOTP(ctx, userID, accountName) } -func (repo *UserRepo) AddMyMfaOTP(ctx context.Context) (*model.OTP, error) { +func (repo *UserRepo) AddMyMFAOTP(ctx context.Context) (*model.OTP, error) { accountName := "" user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) if err != nil { @@ -286,18 +290,66 @@ func (repo *UserRepo) AddMyMfaOTP(ctx context.Context) (*model.OTP, error) { return repo.UserEvents.AddOTP(ctx, authz.GetCtxData(ctx).UserID, accountName) } -func (repo *UserRepo) VerifyMfaOTPSetup(ctx context.Context, userID, code string) error { - return repo.UserEvents.CheckMfaOTPSetup(ctx, userID, code) +func (repo *UserRepo) VerifyMFAOTPSetup(ctx context.Context, userID, code string) error { + return repo.UserEvents.CheckMFAOTPSetup(ctx, userID, code) } -func (repo *UserRepo) VerifyMyMfaOTPSetup(ctx context.Context, code string) error { - return repo.UserEvents.CheckMfaOTPSetup(ctx, authz.GetCtxData(ctx).UserID, code) +func (repo *UserRepo) VerifyMyMFAOTPSetup(ctx context.Context, code string) error { + return repo.UserEvents.CheckMFAOTPSetup(ctx, authz.GetCtxData(ctx).UserID, code) } -func (repo *UserRepo) RemoveMyMfaOTP(ctx context.Context) error { +func (repo *UserRepo) RemoveMyMFAOTP(ctx context.Context) error { return repo.UserEvents.RemoveOTP(ctx, authz.GetCtxData(ctx).UserID) } +func (repo *UserRepo) AddMFAU2F(ctx context.Context, userID string) (*model.WebAuthNToken, error) { + return repo.UserEvents.AddU2F(ctx, userID) +} + +func (repo *UserRepo) AddMyMFAU2F(ctx context.Context) (*model.WebAuthNToken, error) { + return repo.UserEvents.AddU2F(ctx, authz.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) VerifyMFAU2FSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error { + return repo.UserEvents.VerifyU2FSetup(ctx, userID, tokenName, credentialData) +} + +func (repo *UserRepo) VerifyMyMFAU2FSetup(ctx context.Context, tokenName string, credentialData []byte) error { + return repo.UserEvents.VerifyU2FSetup(ctx, authz.GetCtxData(ctx).UserID, tokenName, credentialData) +} + +func (repo *UserRepo) RemoveMFAU2F(ctx context.Context, userID, webAuthNTokenID string) error { + return repo.UserEvents.RemoveU2FToken(ctx, userID, webAuthNTokenID) +} + +func (repo *UserRepo) RemoveMyMFAU2F(ctx context.Context, webAuthNTokenID string) error { + return repo.UserEvents.RemoveU2FToken(ctx, authz.GetCtxData(ctx).UserID, webAuthNTokenID) +} + +func (repo *UserRepo) AddPasswordless(ctx context.Context, userID string) (*model.WebAuthNToken, error) { + return repo.UserEvents.AddPasswordless(ctx, userID) +} + +func (repo *UserRepo) AddMyPasswordless(ctx context.Context) (*model.WebAuthNToken, error) { + return repo.UserEvents.AddPasswordless(ctx, authz.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) VerifyPasswordlessSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error { + return repo.UserEvents.VerifyPasswordlessSetup(ctx, userID, tokenName, credentialData) +} + +func (repo *UserRepo) VerifyMyPasswordlessSetup(ctx context.Context, tokenName string, credentialData []byte) error { + return repo.UserEvents.VerifyPasswordlessSetup(ctx, authz.GetCtxData(ctx).UserID, tokenName, credentialData) +} + +func (repo *UserRepo) RemovePasswordless(ctx context.Context, userID, webAuthNTokenID string) error { + return repo.UserEvents.RemovePasswordlessToken(ctx, userID, webAuthNTokenID) +} + +func (repo *UserRepo) RemoveMyPasswordless(ctx context.Context, webAuthNTokenID string) error { + return repo.UserEvents.RemovePasswordlessToken(ctx, authz.GetCtxData(ctx).UserID, webAuthNTokenID) +} + func (repo *UserRepo) ChangeMyUsername(ctx context.Context, username string) error { ctxData := authz.GetCtxData(ctx) orgPolicy, err := repo.View.OrgIAMPolicyByAggregateID(ctxData.OrgID) @@ -327,8 +379,8 @@ func (repo *UserRepo) VerifyInitCode(ctx context.Context, userID, code, password return repo.UserEvents.VerifyInitCode(ctx, pwPolicyView, userID, code, password) } -func (repo *UserRepo) SkipMfaInit(ctx context.Context, userID string) error { - return repo.UserEvents.SkipMfaInit(ctx, userID) +func (repo *UserRepo) SkipMFAInit(ctx context.Context, userID string) error { + return repo.UserEvents.SkipMFAInit(ctx, userID) } func (repo *UserRepo) RequestPasswordReset(ctx context.Context, loginname string) error { diff --git a/internal/auth/repository/eventsourcing/handler/user.go b/internal/auth/repository/eventsourcing/handler/user.go index 5d04b62a3b..8b1ceb091c 100644 --- a/internal/auth/repository/eventsourcing/handler/user.go +++ b/internal/auth/repository/eventsourcing/handler/user.go @@ -67,7 +67,7 @@ func (u *User) ProcessUser(event *models.Event) (err error) { if err != nil { return err } - u.fillLoginNames(user) + err = u.fillLoginNames(user) case es_model.UserProfileChanged, es_model.UserEmailChanged, es_model.UserEmailVerified, @@ -94,6 +94,12 @@ func (u *User) ProcessUser(event *models.Event) (err error) { es_model.HumanMFAOTPAdded, es_model.HumanMFAOTPVerified, es_model.HumanMFAOTPRemoved, + es_model.HumanMFAU2FTokenAdded, + es_model.HumanMFAU2FTokenVerified, + es_model.HumanMFAU2FTokenRemoved, + es_model.HumanPasswordlessTokenAdded, + es_model.HumanPasswordlessTokenVerified, + es_model.HumanPasswordlessTokenRemoved, es_model.HumanMFAInitSkipped, es_model.MachineChanged, es_model.HumanPasswordChanged: diff --git a/internal/auth/repository/eventsourcing/handler/user_session.go b/internal/auth/repository/eventsourcing/handler/user_session.go index 3931e62f24..174a0d8394 100644 --- a/internal/auth/repository/eventsourcing/handler/user_session.go +++ b/internal/auth/repository/eventsourcing/handler/user_session.go @@ -48,6 +48,10 @@ func (u *UserSession) Reduce(event *models.Event) (err error) { es_model.HumanExternalLoginCheckSucceeded, es_model.HumanMFAOTPCheckSucceeded, es_model.HumanMFAOTPCheckFailed, + es_model.HumanMFAU2FTokenCheckSucceeded, + es_model.HumanMFAU2FTokenCheckFailed, + es_model.HumanPasswordlessTokenCheckSucceeded, + es_model.HumanPasswordlessTokenCheckFailed, es_model.HumanSignedOut: eventData, err := view_model.UserSessionFromEvent(event) if err != nil { @@ -78,7 +82,9 @@ func (u *UserSession) Reduce(event *models.Event) (err error) { es_model.DomainClaimed, es_model.UserUserNameChanged, es_model.HumanExternalIDPRemoved, - es_model.HumanExternalIDPCascadeRemoved: + es_model.HumanExternalIDPCascadeRemoved, + es_model.HumanPasswordlessTokenRemoved, + es_model.HumanMFAU2FTokenRemoved: sessions, err := u.view.UserSessionsByUserID(event.AggregateID) if err != nil { return err diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 3647222c44..5f05be73ed 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -138,7 +138,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au IdGenerator: idGenerator, PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, - MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration, + MFAInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MFAInitSkip.Duration, SecondFactorCheckLifeTime: systemDefaults.VerificationLifetimes.SecondFactorCheck.Duration, MultiFactorCheckLifeTime: systemDefaults.VerificationLifetimes.MultiFactorCheck.Duration, IAMID: systemDefaults.IamID, diff --git a/internal/auth/repository/eventsourcing/view/user.go b/internal/auth/repository/eventsourcing/view/user.go index a0a4c479fb..e8b0591d22 100644 --- a/internal/auth/repository/eventsourcing/view/user.go +++ b/internal/auth/repository/eventsourcing/view/user.go @@ -48,8 +48,8 @@ func (v *View) IsUserUnique(userName, email string) (bool, error) { return view.IsUserUnique(v.Db, userTable, userName, email) } -func (v *View) UserMfas(userID string) ([]*usr_model.MultiFactor, error) { - return view.UserMfas(v.Db, userTable, userID) +func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) { + return view.UserMFAs(v.Db, userTable, userID) } func (v *View) PutUser(user *model.UserView, sequence uint64, eventTimestamp time.Time) error { diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index 164ca23c35..81f6084e4c 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -13,7 +13,7 @@ type UserRepository interface { RegisterExternalUser(ctx context.Context, user *model.User, externalIDP *model.ExternalIDP, member *org_model.OrgMember, resourceOwner string) (*model.User, error) myUserRepo - SkipMfaInit(ctx context.Context, userID string) error + SkipMFAInit(ctx context.Context, userID string) error RequestPasswordReset(ctx context.Context, username string) error SetPassword(ctx context.Context, userID, code, password string) error @@ -25,8 +25,16 @@ type UserRepository interface { VerifyInitCode(ctx context.Context, userID, code, password string) error ResendInitVerificationMail(ctx context.Context, userID string) error - AddMfaOTP(ctx context.Context, userID string) (*model.OTP, error) - VerifyMfaOTPSetup(ctx context.Context, userID, code string) error + AddMFAOTP(ctx context.Context, userID string) (*model.OTP, error) + VerifyMFAOTPSetup(ctx context.Context, userID, code string) error + + AddMFAU2F(ctx context.Context, id string) (*model.WebAuthNToken, error) + VerifyMFAU2FSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error + RemoveMFAU2F(ctx context.Context, userID, webAuthNTokenID string) error + + AddPasswordless(ctx context.Context, id string) (*model.WebAuthNToken, error) + VerifyPasswordlessSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error + RemovePasswordless(ctx context.Context, userID, webAuthNTokenID string) error ChangeUsername(ctx context.Context, userID, username string) error @@ -63,10 +71,18 @@ type myUserRepo interface { AddMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) (*model.ExternalIDP, error) RemoveMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error - MyUserMfas(ctx context.Context) ([]*model.MultiFactor, error) - AddMyMfaOTP(ctx context.Context) (*model.OTP, error) - VerifyMyMfaOTPSetup(ctx context.Context, code string) error - RemoveMyMfaOTP(ctx context.Context) error + MyUserMFAs(ctx context.Context) ([]*model.MultiFactor, error) + AddMyMFAOTP(ctx context.Context) (*model.OTP, error) + VerifyMyMFAOTPSetup(ctx context.Context, code string) error + RemoveMyMFAOTP(ctx context.Context) error + + AddMyMFAU2F(ctx context.Context) (*model.WebAuthNToken, error) + VerifyMyMFAU2FSetup(ctx context.Context, tokenName string, data []byte) error + RemoveMyMFAU2F(ctx context.Context, webAuthNTokenID string) error + + AddMyPasswordless(ctx context.Context) (*model.WebAuthNToken, error) + VerifyMyPasswordlessSetup(ctx context.Context, tokenName string, data []byte) error + RemoveMyPasswordless(ctx context.Context, webAuthNTokenID string) error ChangeMyUsername(ctx context.Context, username string) error diff --git a/internal/auth_request/model/auth_request.go b/internal/auth_request/model/auth_request.go index adf36e27bc..bd51897afd 100644 --- a/internal/auth_request/model/auth_request.go +++ b/internal/auth_request/model/auth_request.go @@ -34,7 +34,7 @@ type AuthRequest struct { LinkingUsers []*ExternalUser PossibleSteps []NextStep PasswordVerified bool - MfasVerified []MFAType + MFAsVerified []MFAType Audience []string AuthTime time.Time Code string @@ -109,7 +109,7 @@ func (a *AuthRequest) IsValid() bool { a.Request != nil && a.Request.IsValid() } -func (a *AuthRequest) MfaLevel() MFALevel { +func (a *AuthRequest) MFALevel() MFALevel { return -1 //PLANNED: check a.PossibleLOAs (and Prompt Login?) } diff --git a/internal/auth_request/model/auth_request_test.go b/internal/auth_request/model/auth_request_test.go index 03319938e4..2e16f9b9a6 100644 --- a/internal/auth_request/model/auth_request_test.go +++ b/internal/auth_request/model/auth_request_test.go @@ -147,7 +147,7 @@ func TestAuthRequest_IsValid(t *testing.T) { } } -func TestAuthRequest_MfaLevel(t *testing.T) { +func TestAuthRequest_MFALevel(t *testing.T) { type fields struct { Prompt Prompt PossibleLOAs []LevelOfAssurance @@ -169,7 +169,7 @@ func TestAuthRequest_MfaLevel(t *testing.T) { Prompt: tt.fields.Prompt, PossibleLOAs: tt.fields.PossibleLOAs, } - if got := a.MfaLevel(); got != tt.want { + if got := a.MFALevel(); got != tt.want { t.Errorf("MFALevel() = %v, want %v", got, tt.want) } }) diff --git a/internal/auth_request/model/next_step.go b/internal/auth_request/model/next_step.go index 028e367127..3312244e11 100644 --- a/internal/auth_request/model/next_step.go +++ b/internal/auth_request/model/next_step.go @@ -15,14 +15,15 @@ const ( NextStepChangePassword NextStepInitPassword NextStepVerifyEmail - NextStepMfaPrompt - NextStepMfaVerify + NextStepMFAPrompt + NextStepMFAVerify NextStepRedirectToCallback NextStepChangeUsername NextStepLinkUsers NextStepExternalNotFoundOption NextStepExternalLogin NextStepGrantRequired + NextStepPasswordless ) type UserSessionState int32 @@ -81,6 +82,12 @@ func (s *ExternalLoginStep) Type() NextStepType { return NextStepExternalLogin } +type PasswordlessStep struct{} + +func (s *PasswordlessStep) Type() NextStepType { + return NextStepPasswordless +} + type ChangePasswordStep struct{} func (s *ChangePasswordStep) Type() NextStepType { @@ -105,21 +112,21 @@ func (s *VerifyEMailStep) Type() NextStepType { return NextStepVerifyEmail } -type MfaPromptStep struct { +type MFAPromptStep struct { Required bool - MfaProviders []MFAType + MFAProviders []MFAType } -func (s *MfaPromptStep) Type() NextStepType { - return NextStepMfaPrompt +func (s *MFAPromptStep) Type() NextStepType { + return NextStepMFAPrompt } -type MfaVerificationStep struct { - MfaProviders []MFAType +type MFAVerificationStep struct { + MFAProviders []MFAType } -func (s *MfaVerificationStep) Type() NextStepType { - return NextStepMfaVerify +func (s *MFAVerificationStep) Type() NextStepType { + return NextStepMFAVerify } type LinkUsersStep struct{} @@ -145,6 +152,7 @@ type MFAType int const ( MFATypeOTP MFAType = iota MFATypeU2F + MFATypeU2FUserVerification ) type MFALevel int diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index 0a5b46c781..c6b582b30d 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -23,6 +23,7 @@ type SystemDefaults struct { DomainVerification DomainVerification IamID string Notifications Notifications + WebAuthN WebAuthN } type ZitadelDocs struct { @@ -52,7 +53,7 @@ type OTPConfig struct { type VerificationLifetimes struct { PasswordCheck types.Duration ExternalLoginCheck types.Duration - MfaInitSkip types.Duration + MFAInitSkip types.Duration SecondFactorCheck types.Duration MultiFactorCheck types.Duration } @@ -89,3 +90,9 @@ type TemplateData struct { VerifyPhone templates.TemplateData DomainClaimed templates.TemplateData } + +type WebAuthN struct { + ID string + Origin string + DisplayName string +} diff --git a/internal/iam/model/iam.go b/internal/iam/model/iam.go index 09610a5c89..f80ea3b400 100644 --- a/internal/iam/model/iam.go +++ b/internal/iam/model/iam.go @@ -14,6 +14,7 @@ const ( Step5 Step6 Step7 + Step8 //StepCount marks the the length of possible steps (StepCount-1 == last possible step) StepCount ) diff --git a/internal/iam/model/login_policy.go b/internal/iam/model/login_policy.go index 265391b5d8..46751498d9 100644 --- a/internal/iam/model/login_policy.go +++ b/internal/iam/model/login_policy.go @@ -16,6 +16,7 @@ type LoginPolicy struct { ForceMFA bool SecondFactors []SecondFactorType MultiFactors []MultiFactorType + PasswordlessType PasswordlessType } type IDPProvider struct { @@ -53,6 +54,13 @@ const ( MultiFactorTypeU2FWithPIN ) +type PasswordlessType int32 + +const ( + PasswordlessTypeNotAllowed PasswordlessType = iota + PasswordlessTypeAllowed +) + func (p *LoginPolicy) IsValid() bool { return p.ObjectRoot.AggregateID != "" } diff --git a/internal/iam/model/login_policy_view.go b/internal/iam/model/login_policy_view.go index c41649984d..4bc6d51850 100644 --- a/internal/iam/model/login_policy_view.go +++ b/internal/iam/model/login_policy_view.go @@ -11,6 +11,7 @@ type LoginPolicyView struct { AllowRegister bool AllowExternalIDP bool ForceMFA bool + PasswordlessType PasswordlessType SecondFactors []SecondFactorType MultiFactors []MultiFactorType Default bool diff --git a/internal/iam/repository/eventsourcing/eventstore_mock_test.go b/internal/iam/repository/eventsourcing/eventstore_mock_test.go index 584e320c45..e41a323014 100644 --- a/internal/iam/repository/eventsourcing/eventstore_mock_test.go +++ b/internal/iam/repository/eventsourcing/eventstore_mock_test.go @@ -127,8 +127,8 @@ func GetMockManipulateIAMWithLoginPolicy(ctrl *gomock.Controller) *IAMEventstore func GetMockManipulateIAMWithLoginPolicyWithMFAs(ctrl *gomock.Controller) *IAMEventstore { policyData, _ := json.Marshal(model.LoginPolicy{AllowRegister: true, AllowUsernamePassword: true, AllowExternalIdp: true}) idpProviderData, _ := json.Marshal(model.IDPProvider{IDPConfigID: "IDPConfigID", Type: 1}) - secondFactor, _ := json.Marshal(model.MFA{MfaType: int32(model2.SecondFactorTypeOTP)}) - multiFactor, _ := json.Marshal(model.MFA{MfaType: int32(model2.MultiFactorTypeU2FWithPIN)}) + secondFactor, _ := json.Marshal(model.MFA{MFAType: int32(model2.SecondFactorTypeOTP)}) + multiFactor, _ := json.Marshal(model.MFA{MFAType: int32(model2.MultiFactorTypeU2FWithPIN)}) events := []*es_models.Event{ {AggregateID: "AggregateID", Sequence: 1, Type: model.IAMSetupStarted}, {AggregateID: "AggregateID", Sequence: 1, Type: model.LoginPolicyAdded, Data: policyData}, diff --git a/internal/iam/repository/eventsourcing/iam.go b/internal/iam/repository/eventsourcing/iam.go index 2522316602..ba62d70c52 100644 --- a/internal/iam/repository/eventsourcing/iam.go +++ b/internal/iam/repository/eventsourcing/iam.go @@ -359,7 +359,7 @@ func LoginPolicySecondFactorAddedAggregate(aggCreator *es_models.AggregateCreato AggregateTypeFilter(model.IAMAggregate). AggregateIDFilter(existing.AggregateID) - validation := checkExistingLoginPolicySecondFactorValidation(mfa.MfaType) + validation := checkExistingLoginPolicySecondFactorValidation(mfa.MFAType) agg.SetPrecondition(validationQuery, validation) return agg.AppendEvent(model.LoginPolicySecondFactorAdded, mfa) } @@ -391,7 +391,7 @@ func LoginPolicyMultiFactorAddedAggregate(aggCreator *es_models.AggregateCreator AggregateTypeFilter(model.IAMAggregate). AggregateIDFilter(existing.AggregateID) - validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MfaType) + validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MFAType) agg.SetPrecondition(validationQuery, validation) return agg.AppendEvent(model.LoginPolicyMultiFactorAdded, mfa) } @@ -689,7 +689,7 @@ func checkExistingLoginPolicySecondFactorValidation(mfaType int32) func(...*es_m if err != nil { return err } - mfas = append(mfas, idp.MfaType) + mfas = append(mfas, idp.MFAType) case model.LoginPolicySecondFactorRemoved: mfa := new(model.MFA) err := mfa.SetData(event) @@ -697,7 +697,7 @@ func checkExistingLoginPolicySecondFactorValidation(mfaType int32) func(...*es_m return err } for i := len(mfas) - 1; i >= 0; i-- { - if mfas[i] == mfa.MfaType { + if mfas[i] == mfa.MFAType { mfas[i] = mfas[len(mfas)-1] mfas[len(mfas)-1] = 0 mfas = mfas[:len(mfas)-1] @@ -726,7 +726,7 @@ func checkExistingLoginPolicyMultiFactorValidation(mfaType int32) func(...*es_mo if err != nil { return err } - mfas = append(mfas, idp.MfaType) + mfas = append(mfas, idp.MFAType) case model.LoginPolicyMultiFactorRemoved: mfa := new(model.MFA) err := mfa.SetData(event) @@ -734,7 +734,7 @@ func checkExistingLoginPolicyMultiFactorValidation(mfaType int32) func(...*es_mo return err } for i := len(mfas) - 1; i >= 0; i-- { - if mfas[i] == mfa.MfaType { + if mfas[i] == mfa.MFAType { mfas[i] = mfas[len(mfas)-1] mfas[len(mfas)-1] = 0 mfas = mfas[:len(mfas)-1] diff --git a/internal/iam/repository/eventsourcing/iam_test.go b/internal/iam/repository/eventsourcing/iam_test.go index 748c27ff81..bf3625dc8c 100644 --- a/internal/iam/repository/eventsourcing/iam_test.go +++ b/internal/iam/repository/eventsourcing/iam_test.go @@ -1497,7 +1497,7 @@ func TestLoginPolicySecondFactorAddedAggregate(t *testing.T) { AllowUsernamePassword: true, }}, newMFA: &model.MFA{ - MfaType: int32(iam_model.SecondFactorTypeOTP), + MFAType: int32(iam_model.SecondFactorTypeOTP), }, aggCreator: models.NewAggregateCreator("Test"), }, @@ -1587,7 +1587,7 @@ func TestLoginPolicySecondFactorRemovedAggregate(t *testing.T) { }, }}, mfa: &model.MFA{ - MfaType: int32(iam_model.SecondFactorTypeOTP), + MFAType: int32(iam_model.SecondFactorTypeOTP), }, aggCreator: models.NewAggregateCreator("Test"), }, @@ -1674,7 +1674,7 @@ func TestLoginPolicyMultiFactorAddedAggregate(t *testing.T) { AllowUsernamePassword: true, }}, newMFA: &model.MFA{ - MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN), + MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN), }, aggCreator: models.NewAggregateCreator("Test"), }, @@ -1764,7 +1764,7 @@ func TestLoginPolicyMultiFactorRemovedAggregate(t *testing.T) { }, }}, mfa: &model.MFA{ - MfaType: int32(iam_model.SecondFactorTypeOTP), + MFAType: int32(iam_model.SecondFactorTypeOTP), }, aggCreator: models.NewAggregateCreator("Test"), }, diff --git a/internal/iam/repository/eventsourcing/model/login_policy.go b/internal/iam/repository/eventsourcing/model/login_policy.go index 6421a86747..7b28b18b96 100644 --- a/internal/iam/repository/eventsourcing/model/login_policy.go +++ b/internal/iam/repository/eventsourcing/model/login_policy.go @@ -14,7 +14,8 @@ type LoginPolicy struct { AllowUsernamePassword bool `json:"allowUsernamePassword"` AllowRegister bool `json:"allowRegister"` AllowExternalIdp bool `json:"allowExternalIdp"` - ForceMFA bool `json:"forceMfa"` + ForceMFA bool `json:"forceMFA"` + PasswordlessType int32 `json:"passwordlessType"` IDPProviders []*IDPProvider `json:"-"` SecondFactors []int32 `json:"-"` MultiFactors []int32 `json:"-"` @@ -31,7 +32,7 @@ type IDPProviderID struct { } type MFA struct { - MfaType int32 `json:"mfaType"` + MFAType int32 `json:"mfaType"` } func GetIDPProvider(providers []*IDPProvider, id string) (int, *IDPProvider) { @@ -65,6 +66,7 @@ func LoginPolicyToModel(policy *LoginPolicy) *iam_model.LoginPolicy { ForceMFA: policy.ForceMFA, SecondFactors: secondFactors, MultiFactors: multiFactors, + PasswordlessType: iam_model.PasswordlessType(policy.PasswordlessType), } } @@ -82,6 +84,7 @@ func LoginPolicyFromModel(policy *iam_model.LoginPolicy) *LoginPolicy { ForceMFA: policy.ForceMFA, SecondFactors: secondFactors, MultiFactors: multiFactors, + PasswordlessType: int32(policy.PasswordlessType), } } @@ -126,7 +129,7 @@ func SecondFactorsFromModel(mfas []iam_model.SecondFactorType) []int32 { } func SecondFactorFromModel(mfa iam_model.SecondFactorType) *MFA { - return &MFA{MfaType: int32(mfa)} + return &MFA{MFAType: int32(mfa)} } func SecondFactorsToModel(mfas []int32) []iam_model.SecondFactorType { @@ -146,7 +149,7 @@ func MultiFactorsFromModel(mfas []iam_model.MultiFactorType) []int32 { } func MultiFactorFromModel(mfa iam_model.MultiFactorType) *MFA { - return &MFA{MfaType: int32(mfa)} + return &MFA{MFAType: int32(mfa)} } func MultiFactorsToModel(mfas []int32) []iam_model.MultiFactorType { @@ -172,6 +175,9 @@ func (p *LoginPolicy) Changes(changed *LoginPolicy) map[string]interface{} { if changed.ForceMFA != p.ForceMFA { changes["forceMFA"] = changed.ForceMFA } + if changed.PasswordlessType != p.PasswordlessType { + changes["passwordlessType"] = changed.PasswordlessType + } return changes } @@ -221,7 +227,7 @@ func (iam *IAM) appendAddSecondFactorToLoginPolicyEvent(event *es_models.Event) if err != nil { return err } - iam.DefaultLoginPolicy.SecondFactors = append(iam.DefaultLoginPolicy.SecondFactors, mfa.MfaType) + iam.DefaultLoginPolicy.SecondFactors = append(iam.DefaultLoginPolicy.SecondFactors, mfa.MFAType) return nil } @@ -231,7 +237,7 @@ func (iam *IAM) appendRemoveSecondFactorFromLoginPolicyEvent(event *es_models.Ev if err != nil { return err } - if i, m := GetMFA(iam.DefaultLoginPolicy.SecondFactors, mfa.MfaType); m != 0 { + if i, m := GetMFA(iam.DefaultLoginPolicy.SecondFactors, mfa.MFAType); m != 0 { iam.DefaultLoginPolicy.SecondFactors[i] = iam.DefaultLoginPolicy.SecondFactors[len(iam.DefaultLoginPolicy.SecondFactors)-1] iam.DefaultLoginPolicy.SecondFactors[len(iam.DefaultLoginPolicy.SecondFactors)-1] = 0 iam.DefaultLoginPolicy.SecondFactors = iam.DefaultLoginPolicy.SecondFactors[:len(iam.DefaultLoginPolicy.SecondFactors)-1] @@ -246,7 +252,7 @@ func (iam *IAM) appendAddMultiFactorToLoginPolicyEvent(event *es_models.Event) e if err != nil { return err } - iam.DefaultLoginPolicy.MultiFactors = append(iam.DefaultLoginPolicy.MultiFactors, mfa.MfaType) + iam.DefaultLoginPolicy.MultiFactors = append(iam.DefaultLoginPolicy.MultiFactors, mfa.MFAType) return nil } @@ -256,7 +262,7 @@ func (iam *IAM) appendRemoveMultiFactorFromLoginPolicyEvent(event *es_models.Eve if err != nil { return err } - if i, m := GetMFA(iam.DefaultLoginPolicy.MultiFactors, mfa.MfaType); m != 0 { + if i, m := GetMFA(iam.DefaultLoginPolicy.MultiFactors, mfa.MFAType); m != 0 { iam.DefaultLoginPolicy.MultiFactors[i] = iam.DefaultLoginPolicy.MultiFactors[len(iam.DefaultLoginPolicy.MultiFactors)-1] iam.DefaultLoginPolicy.MultiFactors[len(iam.DefaultLoginPolicy.MultiFactors)-1] = 0 iam.DefaultLoginPolicy.MultiFactors = iam.DefaultLoginPolicy.MultiFactors[:len(iam.DefaultLoginPolicy.MultiFactors)-1] diff --git a/internal/iam/repository/eventsourcing/model/login_policy_test.go b/internal/iam/repository/eventsourcing/model/login_policy_test.go index cd39fa07e8..a7a9164885 100644 --- a/internal/iam/repository/eventsourcing/model/login_policy_test.go +++ b/internal/iam/repository/eventsourcing/model/login_policy_test.go @@ -275,7 +275,7 @@ func TestAppendAddSecondFactorToPolicyEvent(t *testing.T) { name: "append add second factor to login policy event", args: args{ iam: &IAM{DefaultLoginPolicy: &LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, - mfa: &MFA{MfaType: int32(model.SecondFactorTypeOTP)}, + mfa: &MFA{MFAType: int32(model.SecondFactorTypeOTP)}, event: &es_models.Event{}, }, result: &IAM{DefaultLoginPolicy: &LoginPolicy{ @@ -294,7 +294,7 @@ func TestAppendAddSecondFactorToPolicyEvent(t *testing.T) { if len(tt.result.DefaultLoginPolicy.SecondFactors) != len(tt.args.iam.DefaultLoginPolicy.SecondFactors) { t.Errorf("got wrong second factors len: expected: %v, actual: %v ", len(tt.result.DefaultLoginPolicy.SecondFactors), len(tt.args.iam.DefaultLoginPolicy.SecondFactors)) } - if tt.result.DefaultLoginPolicy.SecondFactors[0] != tt.args.mfa.MfaType { + if tt.result.DefaultLoginPolicy.SecondFactors[0] != tt.args.mfa.MFAType { t.Errorf("got wrong second factor: expected: %v, actual: %v ", tt.result.DefaultLoginPolicy.SecondFactors[0], tt.args.mfa) } }) @@ -320,7 +320,7 @@ func TestRemoveSecondFactorToPolicyEvent(t *testing.T) { SecondFactors: []int32{ int32(model.SecondFactorTypeOTP), }}}, - mfa: &MFA{MfaType: int32(model.SecondFactorTypeOTP)}, + mfa: &MFA{MFAType: int32(model.SecondFactorTypeOTP)}, event: &es_models.Event{}, }, result: &IAM{DefaultLoginPolicy: &LoginPolicy{ @@ -359,7 +359,7 @@ func TestAppendAddMultiFactorToPolicyEvent(t *testing.T) { name: "append add mfa to login policy event", args: args{ iam: &IAM{DefaultLoginPolicy: &LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, - mfa: &MFA{MfaType: int32(model.MultiFactorTypeU2FWithPIN)}, + mfa: &MFA{MFAType: int32(model.MultiFactorTypeU2FWithPIN)}, event: &es_models.Event{}, }, result: &IAM{DefaultLoginPolicy: &LoginPolicy{ @@ -378,7 +378,7 @@ func TestAppendAddMultiFactorToPolicyEvent(t *testing.T) { if len(tt.result.DefaultLoginPolicy.MultiFactors) != len(tt.args.iam.DefaultLoginPolicy.MultiFactors) { t.Errorf("got wrong mfas len: expected: %v, actual: %v ", len(tt.result.DefaultLoginPolicy.MultiFactors), len(tt.args.iam.DefaultLoginPolicy.MultiFactors)) } - if tt.result.DefaultLoginPolicy.MultiFactors[0] != tt.args.mfa.MfaType { + if tt.result.DefaultLoginPolicy.MultiFactors[0] != tt.args.mfa.MFAType { t.Errorf("got wrong mfa: expected: %v, actual: %v ", tt.result.DefaultLoginPolicy.MultiFactors[0], tt.args.mfa) } }) @@ -404,7 +404,7 @@ func TestRemoveMultiFactorToPolicyEvent(t *testing.T) { MultiFactors: []int32{ int32(model.MultiFactorTypeU2FWithPIN), }}}, - mfa: &MFA{MfaType: int32(model.MultiFactorTypeU2FWithPIN)}, + mfa: &MFA{MFAType: int32(model.MultiFactorTypeU2FWithPIN)}, event: &es_models.Event{}, }, result: &IAM{DefaultLoginPolicy: &LoginPolicy{ diff --git a/internal/iam/repository/view/model/login_policy.go b/internal/iam/repository/view/model/login_policy.go index 9977c637bd..7bdc919a87 100644 --- a/internal/iam/repository/view/model/login_policy.go +++ b/internal/iam/repository/view/model/login_policy.go @@ -28,6 +28,7 @@ type LoginPolicyView struct { AllowUsernamePassword bool `json:"allowUsernamePassword" gorm:"column:allow_username_password"` AllowExternalIDP bool `json:"allowExternalIdp" gorm:"column:allow_external_idp"` ForceMFA bool `json:"forceMFA" gorm:"column:force_mfa"` + PasswordlessType int32 `json:"passwordlessType" gorm:"column:passwordless_type"` SecondFactors pq.Int64Array `json:"-" gorm:"column:second_factors"` MultiFactors pq.Int64Array `json:"-" gorm:"column:multi_factors"` Default bool `json:"-" gorm:"-"` @@ -45,6 +46,7 @@ func LoginPolicyViewFromModel(policy *model.LoginPolicyView) *LoginPolicyView { AllowExternalIDP: policy.AllowExternalIDP, AllowUsernamePassword: policy.AllowUsernamePassword, ForceMFA: policy.ForceMFA, + PasswordlessType: int32(policy.PasswordlessType), SecondFactors: secondFactorsFromModel(policy.SecondFactors), MultiFactors: multiFactorsFromModel(policy.MultiFactors), Default: policy.Default, @@ -77,6 +79,7 @@ func LoginPolicyViewToModel(policy *LoginPolicyView) *model.LoginPolicyView { AllowExternalIDP: policy.AllowExternalIDP, AllowUsernamePassword: policy.AllowUsernamePassword, ForceMFA: policy.ForceMFA, + PasswordlessType: model.PasswordlessType(policy.PasswordlessType), SecondFactors: secondFactorsToModel(policy.SecondFactors), MultiFactors: multiFactorsToToModel(policy.MultiFactors), Default: policy.Default, @@ -115,7 +118,7 @@ func (p *LoginPolicyView) AppendEvent(event *models.Event) (err error) { if err != nil { return err } - p.SecondFactors = append(p.SecondFactors, int64(mfa.MfaType)) + p.SecondFactors = append(p.SecondFactors, int64(mfa.MFAType)) case es_model.LoginPolicySecondFactorRemoved, org_es_model.LoginPolicySecondFactorRemoved: err = p.removeSecondFactor(event) case es_model.LoginPolicyMultiFactorAdded, org_es_model.LoginPolicyMultiFactorAdded: @@ -124,7 +127,7 @@ func (p *LoginPolicyView) AppendEvent(event *models.Event) (err error) { if err != nil { return err } - p.MultiFactors = append(p.MultiFactors, int64(mfa.MfaType)) + p.MultiFactors = append(p.MultiFactors, int64(mfa.MFAType)) case es_model.LoginPolicyMultiFactorRemoved, org_es_model.LoginPolicyMultiFactorRemoved: err = p.removeMultiFactor(event) } @@ -150,7 +153,7 @@ func (p *LoginPolicyView) removeSecondFactor(event *models.Event) error { return err } for i := len(p.SecondFactors) - 1; i >= 0; i-- { - if p.SecondFactors[i] == int64(mfa.MfaType) { + if p.SecondFactors[i] == int64(mfa.MFAType) { copy(p.SecondFactors[i:], p.SecondFactors[i+1:]) p.SecondFactors[len(p.SecondFactors)-1] = 0 p.SecondFactors = p.SecondFactors[:len(p.SecondFactors)-1] @@ -167,7 +170,7 @@ func (p *LoginPolicyView) removeMultiFactor(event *models.Event) error { return err } for i := len(p.MultiFactors) - 1; i >= 0; i-- { - if p.MultiFactors[i] == int64(mfa.MfaType) { + if p.MultiFactors[i] == int64(mfa.MFAType) { copy(p.MultiFactors[i:], p.MultiFactors[i+1:]) p.MultiFactors[len(p.MultiFactors)-1] = 0 p.MultiFactors = p.MultiFactors[:len(p.MultiFactors)-1] diff --git a/internal/management/repository/eventsourcing/eventstore/user.go b/internal/management/repository/eventsourcing/eventstore/user.go index 576dedd367..3be494fd68 100644 --- a/internal/management/repository/eventsourcing/eventstore/user.go +++ b/internal/management/repository/eventsourcing/eventstore/user.go @@ -209,7 +209,7 @@ func (repo *UserRepo) IsUserUnique(ctx context.Context, userName, email string) return repo.View.IsUserUnique(userName, email) } -func (repo *UserRepo) UserMfas(ctx context.Context, userID string) ([]*usr_model.MultiFactor, error) { +func (repo *UserRepo) UserMFAs(ctx context.Context, userID string) ([]*usr_model.MultiFactor, error) { user, err := repo.UserByID(ctx, userID) if err != nil { return nil, err @@ -217,10 +217,10 @@ func (repo *UserRepo) UserMfas(ctx context.Context, userID string) ([]*usr_model if user.HumanView == nil { return nil, errors.ThrowPreconditionFailed(nil, "EVENT-xx0hV", "Errors.User.NotHuman") } - if user.OTPState == usr_model.MfaStateUnspecified { + if user.OTPState == usr_model.MFAStateUnspecified { return []*usr_model.MultiFactor{}, nil } - return []*usr_model.MultiFactor{{Type: usr_model.MfaTypeOTP, State: user.OTPState}}, nil + return []*usr_model.MultiFactor{{Type: usr_model.MFATypeOTP, State: user.OTPState}}, nil } func (repo *UserRepo) RemoveOTP(ctx context.Context, userID string) error { diff --git a/internal/management/repository/eventsourcing/handler/user.go b/internal/management/repository/eventsourcing/handler/user.go index 7d7f53ad78..0721e0fcd3 100644 --- a/internal/management/repository/eventsourcing/handler/user.go +++ b/internal/management/repository/eventsourcing/handler/user.go @@ -91,6 +91,12 @@ func (u *User) ProcessUser(event *models.Event) (err error) { es_model.HumanMFAOTPAdded, es_model.HumanMFAOTPVerified, es_model.HumanMFAOTPRemoved, + es_model.HumanMFAU2FTokenAdded, + es_model.HumanMFAU2FTokenVerified, + es_model.HumanMFAU2FTokenRemoved, + es_model.HumanPasswordlessTokenAdded, + es_model.HumanPasswordlessTokenVerified, + es_model.HumanPasswordlessTokenRemoved, es_model.MachineChanged: user, err = u.view.UserByID(event.AggregateID) if err != nil { diff --git a/internal/management/repository/eventsourcing/view/user.go b/internal/management/repository/eventsourcing/view/user.go index ec440e0221..69fc14a270 100644 --- a/internal/management/repository/eventsourcing/view/user.go +++ b/internal/management/repository/eventsourcing/view/user.go @@ -36,8 +36,8 @@ func (v *View) IsUserUnique(userName, email string) (bool, error) { return view.IsUserUnique(v.Db, userTable, userName, email) } -func (v *View) UserMfas(userID string) ([]*usr_model.MultiFactor, error) { - return view.UserMfas(v.Db, userTable, userID) +func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) { + return view.UserMFAs(v.Db, userTable, userID) } func (v *View) PutUsers(user []*model.UserView, sequence uint64, eventTimestamp time.Time) error { diff --git a/internal/management/repository/user.go b/internal/management/repository/user.go index ae035368e7..92a2364c11 100644 --- a/internal/management/repository/user.go +++ b/internal/management/repository/user.go @@ -30,7 +30,7 @@ type UserRepository interface { ProfileByID(ctx context.Context, userID string) (*model.Profile, error) ChangeProfile(ctx context.Context, profile *model.Profile) (*model.Profile, error) - UserMfas(ctx context.Context, userID string) ([]*model.MultiFactor, error) + UserMFAs(ctx context.Context, userID string) ([]*model.MultiFactor, error) RemoveOTP(ctx context.Context, userID string) error SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) diff --git a/internal/org/repository/eventsourcing/eventstore.go b/internal/org/repository/eventsourcing/eventstore.go index 8db5c785eb..c312c5e808 100644 --- a/internal/org/repository/eventsourcing/eventstore.go +++ b/internal/org/repository/eventsourcing/eventstore.go @@ -905,7 +905,7 @@ func (es *OrgEventstore) AddSecondFactorToLoginPolicy(ctx context.Context, aggre if err != nil { return 0, err } - if _, m := iam_es_model.GetMFA(repoOrg.LoginPolicy.SecondFactors, repoMFA.MfaType); m != 0 { + if _, m := iam_es_model.GetMFA(repoOrg.LoginPolicy.SecondFactors, repoMFA.MFAType); m != 0 { return iam_model.SecondFactorType(m), nil } return 0, errors.ThrowInternal(nil, "EVENT-rM9so", "Errors.Internal") @@ -950,7 +950,7 @@ func (es *OrgEventstore) AddMultiFactorToLoginPolicy(ctx context.Context, aggreg if err != nil { return 0, err } - if _, m := iam_es_model.GetMFA(repoOrg.LoginPolicy.MultiFactors, repoMFA.MfaType); m != 0 { + if _, m := iam_es_model.GetMFA(repoOrg.LoginPolicy.MultiFactors, repoMFA.MFAType); m != 0 { return iam_model.MultiFactorType(m), nil } return 0, errors.ThrowInternal(nil, "EVENT-2fMo0", "Errors.Internal") diff --git a/internal/org/repository/eventsourcing/eventstore_mock_test.go b/internal/org/repository/eventsourcing/eventstore_mock_test.go index f8aa7fb6cb..6d7d175317 100644 --- a/internal/org/repository/eventsourcing/eventstore_mock_test.go +++ b/internal/org/repository/eventsourcing/eventstore_mock_test.go @@ -109,8 +109,8 @@ func GetMockChangesOrgWithLoginPolicyWithMFA(ctrl *gomock.Controller) *OrgEvents orgData, _ := json.Marshal(model.Org{Name: "MusterOrg"}) loginPolicy, _ := json.Marshal(iam_es_model.LoginPolicy{AllowRegister: true, AllowExternalIdp: true, AllowUsernamePassword: true}) idpData, _ := json.Marshal(iam_es_model.IDPProvider{IDPConfigID: "IDPConfigID", Type: int32(iam_model.IDPProviderTypeSystem)}) - secondFactor, _ := json.Marshal(iam_es_model.MFA{MfaType: int32(iam_model.SecondFactorTypeOTP)}) - multiFactor, _ := json.Marshal(iam_es_model.MFA{MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN)}) + secondFactor, _ := json.Marshal(iam_es_model.MFA{MFAType: int32(iam_model.SecondFactorTypeOTP)}) + multiFactor, _ := json.Marshal(iam_es_model.MFA{MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN)}) events := []*es_models.Event{ {AggregateID: "AggregateID", Sequence: 1, Type: model.OrgAdded, Data: orgData}, {AggregateID: "AggregateID", Sequence: 1, Type: model.LoginPolicyAdded, Data: loginPolicy}, diff --git a/internal/org/repository/eventsourcing/login_policy.go b/internal/org/repository/eventsourcing/login_policy.go index bc25997395..def04a04b0 100644 --- a/internal/org/repository/eventsourcing/login_policy.go +++ b/internal/org/repository/eventsourcing/login_policy.go @@ -105,7 +105,7 @@ func LoginPolicySecondFactorAddedAggregate(aggCreator *es_models.AggregateCreato AggregateTypeFilter(model.OrgAggregate). AggregateIDsFilter(org.AggregateID) - validation := checkExistingLoginPolicySecondFactorValidation(mfa.MfaType) + validation := checkExistingLoginPolicySecondFactorValidation(mfa.MFAType) agg.SetPrecondition(validationQuery, validation) return agg.AppendEvent(model.LoginPolicySecondFactorAdded, mfa) } @@ -137,7 +137,7 @@ func LoginPolicyMultiFactorAddedAggregate(aggCreator *es_models.AggregateCreator AggregateTypeFilter(model.OrgAggregate). AggregateIDsFilter(org.AggregateID) - validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MfaType) + validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MFAType) agg.SetPrecondition(validationQuery, validation) return agg.AppendEvent(model.LoginPolicyMultiFactorAdded, mfa) } @@ -261,7 +261,7 @@ func checkExistingLoginPolicySecondFactorValidation(mfaType int32) func(...*es_m if err != nil { return err } - mfas = append(mfas, mfa.MfaType) + mfas = append(mfas, mfa.MFAType) case model.LoginPolicySecondFactorRemoved: idp := new(iam_es_model.IDPProvider) err := idp.SetData(event) @@ -301,7 +301,7 @@ func checkExistingLoginPolicyMultiFactorValidation(mfaType int32) func(...*es_mo if err != nil { return err } - mfas = append(mfas, mfa.MfaType) + mfas = append(mfas, mfa.MFAType) case model.LoginPolicyMultiFactorRemoved: idp := new(iam_es_model.IDPProvider) err := idp.SetData(event) diff --git a/internal/org/repository/eventsourcing/login_policy_test.go b/internal/org/repository/eventsourcing/login_policy_test.go index 838b8b28c2..fb11d413cb 100644 --- a/internal/org/repository/eventsourcing/login_policy_test.go +++ b/internal/org/repository/eventsourcing/login_policy_test.go @@ -472,7 +472,7 @@ func TestLoginPolicySecondFactorAddedAggregate(t *testing.T) { ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, }, new: &iam_es_model.MFA{ - MfaType: int32(iam_model.SecondFactorTypeOTP), + MFAType: int32(iam_model.SecondFactorTypeOTP), }, aggCreator: models.NewAggregateCreator("Test"), }, @@ -562,7 +562,7 @@ func TestLoginPolicySecondFactorRemovedAggregate(t *testing.T) { }, }}, new: &iam_es_model.MFA{ - MfaType: int32(iam_model.SecondFactorTypeOTP), + MFAType: int32(iam_model.SecondFactorTypeOTP), }, aggCreator: models.NewAggregateCreator("Test"), }, @@ -645,7 +645,7 @@ func TestLoginPolicyMultiFactorAddedAggregate(t *testing.T) { ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, }, new: &iam_es_model.MFA{ - MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN), + MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN), }, aggCreator: models.NewAggregateCreator("Test"), }, @@ -735,7 +735,7 @@ func TestLoginPolicyMultiFactorRemovedAggregate(t *testing.T) { }, }}, new: &iam_es_model.MFA{ - MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN), + MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN), }, aggCreator: models.NewAggregateCreator("Test"), }, diff --git a/internal/org/repository/eventsourcing/model/login_policy.go b/internal/org/repository/eventsourcing/model/login_policy.go index ab1ca319ce..02388669f8 100644 --- a/internal/org/repository/eventsourcing/model/login_policy.go +++ b/internal/org/repository/eventsourcing/model/login_policy.go @@ -55,7 +55,7 @@ func (o *Org) appendAddSecondFactorToLoginPolicyEvent(event *es_models.Event) er if err != nil { return err } - o.LoginPolicy.SecondFactors = append(o.LoginPolicy.SecondFactors, mfa.MfaType) + o.LoginPolicy.SecondFactors = append(o.LoginPolicy.SecondFactors, mfa.MFAType) return nil } @@ -65,7 +65,7 @@ func (o *Org) appendRemoveSecondFactorFromLoginPolicyEvent(event *es_models.Even if err != nil { return err } - if i, m := iam_es_model.GetMFA(o.LoginPolicy.SecondFactors, mfa.MfaType); m != 0 { + if i, m := iam_es_model.GetMFA(o.LoginPolicy.SecondFactors, mfa.MFAType); m != 0 { o.LoginPolicy.SecondFactors[i] = o.LoginPolicy.SecondFactors[len(o.LoginPolicy.SecondFactors)-1] o.LoginPolicy.SecondFactors[len(o.LoginPolicy.SecondFactors)-1] = 0 o.LoginPolicy.SecondFactors = o.LoginPolicy.SecondFactors[:len(o.LoginPolicy.SecondFactors)-1] @@ -80,7 +80,7 @@ func (o *Org) appendAddMultiFactorToLoginPolicyEvent(event *es_models.Event) err if err != nil { return err } - o.LoginPolicy.MultiFactors = append(o.LoginPolicy.MultiFactors, mfa.MfaType) + o.LoginPolicy.MultiFactors = append(o.LoginPolicy.MultiFactors, mfa.MFAType) return nil } @@ -90,7 +90,7 @@ func (o *Org) appendRemoveMultiFactorFromLoginPolicyEvent(event *es_models.Event if err != nil { return err } - if i, m := iam_es_model.GetMFA(o.LoginPolicy.MultiFactors, mfa.MfaType); m != 0 { + if i, m := iam_es_model.GetMFA(o.LoginPolicy.MultiFactors, mfa.MFAType); m != 0 { o.LoginPolicy.MultiFactors[i] = o.LoginPolicy.MultiFactors[len(o.LoginPolicy.MultiFactors)-1] o.LoginPolicy.MultiFactors[len(o.LoginPolicy.MultiFactors)-1] = 0 o.LoginPolicy.MultiFactors = o.LoginPolicy.MultiFactors[:len(o.LoginPolicy.MultiFactors)-1] diff --git a/internal/org/repository/eventsourcing/model/login_policy_test.go b/internal/org/repository/eventsourcing/model/login_policy_test.go index 7ef70e275d..2662cb0910 100644 --- a/internal/org/repository/eventsourcing/model/login_policy_test.go +++ b/internal/org/repository/eventsourcing/model/login_policy_test.go @@ -224,7 +224,7 @@ func TestAppendAddSecondFactorToPolicyEvent(t *testing.T) { name: "append add second factor to login policy event", args: args{ org: &Org{LoginPolicy: &iam_es_model.LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, - mfa: &iam_es_model.MFA{MfaType: int32(iam_model.SecondFactorTypeOTP)}, + mfa: &iam_es_model.MFA{MFAType: int32(iam_model.SecondFactorTypeOTP)}, event: &es_models.Event{}, }, result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ @@ -246,7 +246,7 @@ func TestAppendAddSecondFactorToPolicyEvent(t *testing.T) { if len(tt.result.LoginPolicy.SecondFactors) != len(tt.args.org.LoginPolicy.SecondFactors) { t.Errorf("got wrong second factor len: expected: %v, actual: %v ", len(tt.result.LoginPolicy.SecondFactors), len(tt.args.org.LoginPolicy.SecondFactors)) } - if tt.result.LoginPolicy.SecondFactors[0] != tt.args.mfa.MfaType { + if tt.result.LoginPolicy.SecondFactors[0] != tt.args.mfa.MFAType { t.Errorf("got wrong second factor: expected: %v, actual: %v ", tt.result.LoginPolicy.SecondFactors[0], tt.args.mfa) } }) @@ -275,7 +275,7 @@ func TestRemoveSecondFactorFromPolicyEvent(t *testing.T) { SecondFactors: []int32{ int32(iam_model.SecondFactorTypeOTP), }}}, - mfa: &iam_es_model.MFA{MfaType: int32(iam_model.SecondFactorTypeOTP)}, + mfa: &iam_es_model.MFA{MFAType: int32(iam_model.SecondFactorTypeOTP)}, event: &es_models.Event{}, }, result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ @@ -314,7 +314,7 @@ func TestAppendAddMultiFactorToPolicyEvent(t *testing.T) { name: "append add mfa to login policy event", args: args{ org: &Org{LoginPolicy: &iam_es_model.LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, - mfa: &iam_es_model.MFA{MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN)}, + mfa: &iam_es_model.MFA{MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN)}, event: &es_models.Event{}, }, result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ @@ -336,7 +336,7 @@ func TestAppendAddMultiFactorToPolicyEvent(t *testing.T) { if len(tt.result.LoginPolicy.MultiFactors) != len(tt.args.org.LoginPolicy.MultiFactors) { t.Errorf("got wrong second factor len: expected: %v, actual: %v ", len(tt.result.LoginPolicy.MultiFactors), len(tt.args.org.LoginPolicy.MultiFactors)) } - if tt.result.LoginPolicy.MultiFactors[0] != tt.args.mfa.MfaType { + if tt.result.LoginPolicy.MultiFactors[0] != tt.args.mfa.MFAType { t.Errorf("got wrong second factor: expected: %v, actual: %v ", tt.result.LoginPolicy.MultiFactors[0], tt.args.mfa) } }) @@ -365,7 +365,7 @@ func TestRemoveMultiFactorFromPolicyEvent(t *testing.T) { MultiFactors: []int32{ int32(iam_model.MultiFactorTypeU2FWithPIN), }}}, - mfa: &iam_es_model.MFA{MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN)}, + mfa: &iam_es_model.MFA{MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN)}, event: &es_models.Event{}, }, result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ diff --git a/internal/project/repository/eventsourcing/eventstore.go b/internal/project/repository/eventsourcing/eventstore.go index d9b3f861bb..8967c81389 100644 --- a/internal/project/repository/eventsourcing/eventstore.go +++ b/internal/project/repository/eventsourcing/eventstore.go @@ -20,6 +20,7 @@ import ( "github.com/caos/zitadel/internal/id" proj_model "github.com/caos/zitadel/internal/project/model" "github.com/caos/zitadel/internal/project/repository/eventsourcing/model" + "github.com/caos/zitadel/internal/telemetry/tracing" ) const ( @@ -788,7 +789,9 @@ func (es *ProjectEventstore) ChangeOIDCConfigSecret(ctx context.Context, project return nil, caos_errs.ThrowInternal(nil, "EVENT-dk87s", "Errors.Internal") } -func (es *ProjectEventstore) VerifyOIDCClientSecret(ctx context.Context, projectID, appID string, secret string) error { +func (es *ProjectEventstore) VerifyOIDCClientSecret(ctx context.Context, projectID, appID string, secret string) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() if appID == "" { return caos_errs.ThrowPreconditionFailed(nil, "EVENT-H3RT2", "Errors.Project.RequiredFieldsMissing") } @@ -804,7 +807,10 @@ func (es *ProjectEventstore) VerifyOIDCClientSecret(ctx context.Context, project return caos_errs.ThrowPreconditionFailed(nil, "EVENT-huywq", "Errors.Project.AppIsNotOIDC") } - if err := crypto.CompareHash(app.OIDCConfig.ClientSecret, []byte(secret), es.passwordAlg); err == nil { + ctx, spanHash := tracing.NewSpan(ctx) + err = crypto.CompareHash(app.OIDCConfig.ClientSecret, []byte(secret), es.passwordAlg) + spanHash.EndWithError(err) + if err == nil { return es.setOIDCClientSecretCheckResult(ctx, existingProject, app.AppID, OIDCClientSecretCheckSucceededAggregate) } if err := es.setOIDCClientSecretCheckResult(ctx, existingProject, app.AppID, OIDCClientSecretCheckFailedAggregate); err != nil { diff --git a/internal/setup/config.go b/internal/setup/config.go index 423b999695..93e0d4926b 100644 --- a/internal/setup/config.go +++ b/internal/setup/config.go @@ -13,6 +13,7 @@ type IAMSetUp struct { Step5 *Step5 Step6 *Step6 Step7 *Step7 + Step8 *Step8 } func (setup *IAMSetUp) steps(currentDone iam_model.Step) ([]step, error) { @@ -27,6 +28,7 @@ func (setup *IAMSetUp) steps(currentDone iam_model.Step) ([]step, error) { setup.Step5, setup.Step6, setup.Step7, + setup.Step8, } { if step.step() <= currentDone { continue diff --git a/internal/setup/step7.go b/internal/setup/step7.go index 210fa8fb56..99d445911c 100644 --- a/internal/setup/step7.go +++ b/internal/setup/step7.go @@ -32,23 +32,23 @@ func (step *Step7) init(setup *Setup) { func (step *Step7) execute(ctx context.Context) (*iam_model.IAM, error) { iam, agg, err := step.add2FAToPolicy(ctx, step.DefaultSecondFactor) if err != nil { - logging.Log("SETUP-ZTuS1").WithField("step", step.step()).WithError(err).Error("unable to finish setup (add default mfa to login policy)") + logging.Log("SETUP-GBD32").WithField("step", step.step()).WithError(err).Error("unable to finish setup (add default mfa to login policy)") return nil, err } iam, agg, push, err := step.setup.IamEvents.PrepareSetupDone(ctx, iam, agg, step.step()) if err != nil { - logging.Log("SETUP-OkF8o").WithField("step", step.step()).WithError(err).Error("unable to finish setup (prepare setup done)") + logging.Log("SETUP-BHrth").WithField("step", step.step()).WithError(err).Error("unable to finish setup (prepare setup done)") return nil, err } err = es_sdk.PushAggregates(ctx, push, iam.AppendEvents, agg) if err != nil { - logging.Log("SETUP-YbQ6T").WithField("step", step.step()).WithError(err).Error("unable to finish setup") + logging.Log("SETUP-k2fla").WithField("step", step.step()).WithError(err).Error("unable to finish setup") return nil, err } return iam_es_model.IAMToModel(iam), nil } func (step *Step7) add2FAToPolicy(ctx context.Context, secondFactor iam_model.SecondFactorType) (*iam_es_model.IAM, *models.Aggregate, error) { - logging.Log("SETUP-geMGDuZ").Info("adding 2FA to loginPolicy") + logging.Log("SETUP-Bew1a").Info("adding 2FA to loginPolicy") return step.setup.IamEvents.PrepareAddSecondFactorToLoginPolicy(ctx, step.setup.iamID, secondFactor) } diff --git a/internal/setup/step8.go b/internal/setup/step8.go new file mode 100644 index 0000000000..30f70a41fb --- /dev/null +++ b/internal/setup/step8.go @@ -0,0 +1,54 @@ +package setup + +import ( + "context" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/eventstore/models" + es_sdk "github.com/caos/zitadel/internal/eventstore/sdk" + iam_model "github.com/caos/zitadel/internal/iam/model" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" +) + +type Step8 struct { + DefaultSecondFactor iam_model.SecondFactorType + + setup *Setup +} + +func (step *Step8) isNil() bool { + return step == nil +} + +func (step *Step8) step() iam_model.Step { + return iam_model.Step8 +} + +func (step *Step8) init(setup *Setup) { + step.setup = setup +} + +func (step *Step8) execute(ctx context.Context) (*iam_model.IAM, error) { + iam, agg, err := step.add2FAToPolicy(ctx, step.DefaultSecondFactor) + if err != nil { + logging.Log("SETUP-Gdbjq").WithField("step", step.step()).WithError(err).Error("unable to finish setup (add default mfa to login policy)") + return nil, err + } + iam, agg, push, err := step.setup.IamEvents.PrepareSetupDone(ctx, iam, agg, step.step()) + if err != nil { + logging.Log("SETUP-Cnf21").WithField("step", step.step()).WithError(err).Error("unable to finish setup (prepare setup done)") + return nil, err + } + err = es_sdk.PushAggregates(ctx, push, iam.AppendEvents, agg) + if err != nil { + logging.Log("SETUP-NFq21").WithField("step", step.step()).WithError(err).Error("unable to finish setup") + return nil, err + } + return iam_es_model.IAMToModel(iam), nil +} + +func (step *Step8) add2FAToPolicy(ctx context.Context, secondFactor iam_model.SecondFactorType) (*iam_es_model.IAM, *models.Aggregate, error) { + logging.Log("SETUP-Bfhb2").Info("adding 2FA to loginPolicy") + return step.setup.IamEvents.PrepareAddSecondFactorToLoginPolicy(ctx, step.setup.iamID, secondFactor) +} diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index a32ac9f4e3..729e781d98 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -53,12 +53,25 @@ Errors: IDPConfigNotExisting: IDP Provider ungültig für diese Organisation NotAllowed: Externer IDP ist auf dieser Organisation nicht erlaubt. MinimumExternalIDPNeeded: Mindestens ein IDP muss hinzugefügt werden. - Mfa: - Otp: + MFA: + OTP: AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht NotReady: Multifaktor OTP (OneTimePassword) ist nicht bereit InvalidCode: Code ist ungültig + U2F: + NotExisting: U2F existiert nicht + Passwordless: + NotExisting: Passwortlos existiert nicht + WebAuthN: + NotFound: WebAuthN Token konnte nicht gefunden werden + BeginRegisterFailed: Es ist ein Fehler bei der WebAuthN Registrierung aufgetreten + MarshalError: Daten konnten nicht umgewandelt werden + ErrorOnParseCredential: Zugangsdaten konnten nicht geparsed werden + CreateCredentialFailed: Zugangsdaten konnten nicht gespeichert werden + BeginLoginFailed: Es ist ein Fehler beim WebAuthN Login aufgetreten + ValidateLoginFailed: Zugangsdaten konnten nicht validiert werden + CloneWarning: Authentifizierungsdaten wurden möglicherweise geklont Org: Invalid: Organisation ist ungültig AlreadyDeactivated: Organisation ist bereits deaktiviert diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 08579d24bf..79088a8465 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -53,12 +53,25 @@ Errors: IDPConfigNotExisting: IDP provider invalid for this organisation NotAllowed: External IDP not allowed on this organisation MinimumExternalIDPNeeded: At least one IDP must be added - Mfa: - Otp: + MFA: + OTP: AlreadyReady: Multifactor OTP (OneTimePassword) is already set up NotExisting: Multifactor OTP (OneTimePassword) doesn't exist NotReady: Multifactor OTP (OneTimePassword) isn't ready InvalidCode: Invalid code + U2F: + NotExisting: U2F does not exist + Passwordless: + NotExisting: Passwordless does not exist + WebAuthN: + NotFound: WebAuthN Token could not be found + BeginRegisterFailed: WebAuthN begin registration failed + MarshalError: Error on marshal data + ErrorOnParseCredential: Error on parse credential data + CreateCredentialFailed: Error on create credentials + BeginLoginFailed: WebAuthN begin login failed + ValidateLoginFailed: Error on validate login credentials + CloneWarning: Credentials may be cloned Org: Invalid: Organisation is invalid AlreadyDeactivated: Organisation is already deactivated diff --git a/internal/ui/login/handler/login.go b/internal/ui/login/handler/login.go index 22495627a9..7da2ea9678 100644 --- a/internal/ui/login/handler/login.go +++ b/internal/ui/login/handler/login.go @@ -2,7 +2,6 @@ package handler import ( "context" - "github.com/caos/zitadel/internal/config/systemdefaults" "net" "net/http" @@ -16,6 +15,7 @@ import ( "github.com/caos/zitadel/internal/api/http/middleware" auth_repository "github.com/caos/zitadel/internal/auth/repository" "github.com/caos/zitadel/internal/auth/repository/eventsourcing" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/form" "github.com/caos/zitadel/internal/id" diff --git a/internal/ui/login/handler/mfa_init_done_handler.go b/internal/ui/login/handler/mfa_init_done_handler.go index 69f41af2f5..741a010677 100644 --- a/internal/ui/login/handler/mfa_init_done_handler.go +++ b/internal/ui/login/handler/mfa_init_done_handler.go @@ -7,15 +7,15 @@ import ( ) const ( - tmplMfaInitDone = "mfainitdone" + tmplMFAInitDone = "mfainitdone" ) type mfaInitDoneData struct { } -func (l *Login) renderMfaInitDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaDoneData) { +func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaDoneData) { var errType, errMessage string - data.baseData = l.getBaseData(r, authReq, "Mfa Init Done", errType, errMessage) + data.baseData = l.getBaseData(r, authReq, "MFA Init Done", errType, errMessage) data.profileData = l.getProfileData(authReq) - l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaInitDone], data, nil) + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMFAInitDone], data, nil) } diff --git a/internal/ui/login/handler/mfa_init_u2f.go b/internal/ui/login/handler/mfa_init_u2f.go new file mode 100644 index 0000000000..dccb3a84d4 --- /dev/null +++ b/internal/ui/login/handler/mfa_init_u2f.go @@ -0,0 +1,59 @@ +package handler + +import ( + "encoding/base64" + "net/http" + + "github.com/caos/zitadel/internal/auth_request/model" + user_model "github.com/caos/zitadel/internal/user/model" +) + +const ( + tmplMFAU2FInit = "mfainitu2f" +) + +func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { + var errType, errMessage, credentialData string + var u2f *user_model.WebAuthNToken + if err == nil { + u2f, err = l.authRepo.AddMFAU2F(setContext(r.Context(), authReq.UserOrgID), authReq.UserID) + } + if err != nil { + errMessage = l.getErrorMessage(r, err) + } + if u2f != nil { + credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData) + } + data := &webAuthNData{ + userData: l.getUserData(r, authReq, "Register WebAuthNToken", errType, errMessage), + CredentialCreationData: credentialData, + } + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMFAU2FInit], data, nil) +} + +func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) { + data := new(webAuthNFormData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if data.Recreate { + l.renderRegisterU2F(w, r, authReq, nil) + return + } + credData, err := base64.URLEncoding.DecodeString(data.CredentialData) + if err != nil { + l.renderRegisterU2F(w, r, authReq, err) + return + } + + if err = l.authRepo.VerifyMFAU2FSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Name, credData); err != nil { + l.renderRegisterU2F(w, r, authReq, err) + return + } + done := &mfaDoneData{ + MFAType: model.MFATypeU2F, + } + l.renderMFAInitDone(w, r, authReq, done) +} diff --git a/internal/ui/login/handler/mfa_init_verify_handler.go b/internal/ui/login/handler/mfa_init_verify_handler.go index d043d90ea1..bc07c6ede2 100644 --- a/internal/ui/login/handler/mfa_init_verify_handler.go +++ b/internal/ui/login/handler/mfa_init_verify_handler.go @@ -12,17 +12,17 @@ import ( ) const ( - tmplMfaInitVerify = "mfainitverify" + tmplMFAInitVerify = "mfainitverify" ) type mfaInitVerifyData struct { - MfaType model.MFAType `schema:"mfaType"` + MFAType model.MFAType `schema:"mfaType"` Code string `schema:"code"` URL string `schema:"url"` Secret string `schema:"secret"` } -func (l *Login) handleMfaInitVerify(w http.ResponseWriter, r *http.Request) { +func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) { data := new(mfaInitVerifyData) authReq, err := l.getAuthRequestAndParseData(r, data) if err != nil { @@ -30,29 +30,29 @@ func (l *Login) handleMfaInitVerify(w http.ResponseWriter, r *http.Request) { return } var verifyData *mfaVerifyData - switch data.MfaType { + switch data.MFAType { case model.MFATypeOTP: - verifyData = l.handleOtpVerify(w, r, authReq, data) + verifyData = l.handleOTPVerify(w, r, authReq, data) } if verifyData != nil { - l.renderMfaInitVerify(w, r, authReq, verifyData, err) + l.renderMFAInitVerify(w, r, authReq, verifyData, err) return } done := &mfaDoneData{ - MfaType: data.MfaType, + MFAType: data.MFAType, } - l.renderMfaInitDone(w, r, authReq, done) + l.renderMFAInitDone(w, r, authReq, done) } -func (l *Login) handleOtpVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaInitVerifyData) *mfaVerifyData { - err := l.authRepo.VerifyMfaOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code) +func (l *Login) handleOTPVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaInitVerifyData) *mfaVerifyData { + err := l.authRepo.VerifyMFAOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code) if err == nil { return nil } mfadata := &mfaVerifyData{ - MfaType: data.MfaType, + MFAType: data.MFAType, otpData: otpData{ Secret: data.Secret, Url: data.URL, @@ -62,21 +62,21 @@ func (l *Login) handleOtpVerify(w http.ResponseWriter, r *http.Request, authReq return mfadata } -func (l *Login) renderMfaInitVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData, err error) { +func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData, err error) { var errType, errMessage string if err != nil { errMessage = l.getErrorMessage(r, err) } - data.baseData = l.getBaseData(r, authReq, "Mfa Init Verify", errType, errMessage) + data.baseData = l.getBaseData(r, authReq, "MFA Init Verify", errType, errMessage) data.profileData = l.getProfileData(authReq) - if data.MfaType == model.MFATypeOTP { + if data.MFAType == model.MFATypeOTP { code, err := generateQrCode(data.otpData.Url) if err == nil { data.otpData.QrCode = code } } - l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaInitVerify], data, nil) + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMFAInitVerify], data, nil) } func generateQrCode(url string) (string, error) { diff --git a/internal/ui/login/handler/mfa_prompt_handler.go b/internal/ui/login/handler/mfa_prompt_handler.go index 516d1d04a3..32a32a00db 100644 --- a/internal/ui/login/handler/mfa_prompt_handler.go +++ b/internal/ui/login/handler/mfa_prompt_handler.go @@ -8,15 +8,15 @@ import ( ) const ( - tmplMfaPrompt = "mfaprompt" + tmplMFAPrompt = "mfaprompt" ) type mfaPromptData struct { - MfaProvider model.MFAType `schema:"provider"` + MFAProvider model.MFAType `schema:"provider"` Skip bool `schema:"skip"` } -func (l *Login) handleMfaPrompt(w http.ResponseWriter, r *http.Request) { +func (l *Login) handleMFAPrompt(w http.ResponseWriter, r *http.Request) { data := new(mfaPromptData) authReq, err := l.getAuthRequestAndParseData(r, data) if err != nil { @@ -25,11 +25,11 @@ func (l *Login) handleMfaPrompt(w http.ResponseWriter, r *http.Request) { } if !data.Skip { mfaVerifyData := new(mfaVerifyData) - mfaVerifyData.MfaType = data.MfaProvider - l.handleMfaCreation(w, r, authReq, mfaVerifyData) + mfaVerifyData.MFAType = data.MFAProvider + l.handleMFACreation(w, r, authReq, mfaVerifyData) return } - err = l.authRepo.SkipMfaInit(setContext(r.Context(), authReq.UserOrgID), authReq.UserID) + err = l.authRepo.SkipMFAInit(setContext(r.Context(), authReq.UserOrgID), authReq.UserID) if err != nil { l.renderError(w, r, authReq, err) return @@ -37,7 +37,7 @@ func (l *Login) handleMfaPrompt(w http.ResponseWriter, r *http.Request) { l.handleLogin(w, r) } -func (l *Login) handleMfaPromptSelection(w http.ResponseWriter, r *http.Request) { +func (l *Login) handleMFAPromptSelection(w http.ResponseWriter, r *http.Request) { data := new(mfaPromptData) authReq, err := l.getAuthRequestAndParseData(r, data) if err != nil { @@ -48,45 +48,48 @@ func (l *Login) handleMfaPromptSelection(w http.ResponseWriter, r *http.Request) l.renderNextStep(w, r, authReq) } -func (l *Login) renderMfaPrompt(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, mfaPromptData *model.MfaPromptStep, err error) { +func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, mfaPromptData *model.MFAPromptStep, err error) { var errType, errMessage string if err != nil { errMessage = l.getErrorMessage(r, err) } data := mfaData{ - baseData: l.getBaseData(r, authReq, "Mfa Prompt", errType, errMessage), + baseData: l.getBaseData(r, authReq, "MFA Prompt", errType, errMessage), profileData: l.getProfileData(authReq), } if mfaPromptData == nil { - l.renderError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-XU0tj", "Errors.User.Mfa.NoProviders")) + l.renderError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-XU0tj", "Errors.User.MFA.NoProviders")) return } - data.MfaProviders = mfaPromptData.MfaProviders - data.MfaRequired = mfaPromptData.Required + data.MFAProviders = mfaPromptData.MFAProviders + data.MFARequired = mfaPromptData.Required - if len(mfaPromptData.MfaProviders) == 1 && mfaPromptData.Required { + if len(mfaPromptData.MFAProviders) == 1 && mfaPromptData.Required { data := &mfaVerifyData{ - MfaType: mfaPromptData.MfaProviders[0], + MFAType: mfaPromptData.MFAProviders[0], } - l.handleMfaCreation(w, r, authReq, data) + l.handleMFACreation(w, r, authReq, data) return } - l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaPrompt], data, nil) + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMFAPrompt], data, nil) } -func (l *Login) handleMfaCreation(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData) { - switch data.MfaType { +func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData) { + switch data.MFAType { case model.MFATypeOTP: - l.handleOtpCreation(w, r, authReq, data) + l.handleOTPCreation(w, r, authReq, data) + return + case model.MFATypeU2F: + l.renderRegisterU2F(w, r, authReq, nil) return } - l.renderError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-Or3HO", "Errors.User.Mfa.NoProviders")) + 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 *model.AuthRequest, data *mfaVerifyData) { - otp, err := l.authRepo.AddMfaOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID) +func (l *Login) handleOTPCreation(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData) { + otp, err := l.authRepo.AddMFAOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID) if err != nil { l.renderError(w, r, authReq, err) return @@ -96,5 +99,5 @@ func (l *Login) handleOtpCreation(w http.ResponseWriter, r *http.Request, authRe Secret: otp.SecretString, Url: otp.Url, } - l.renderMfaInitVerify(w, r, authReq, data, nil) + l.renderMFAInitVerify(w, r, authReq, data, nil) } diff --git a/internal/ui/login/handler/mfa_verify_handler.go b/internal/ui/login/handler/mfa_verify_handler.go index 3c22f854ae..d2819afd1c 100644 --- a/internal/ui/login/handler/mfa_verify_handler.go +++ b/internal/ui/login/handler/mfa_verify_handler.go @@ -8,24 +8,24 @@ import ( ) const ( - tmplMfaVerify = "mfaverify" + tmplMFAVerify = "mfaverify" ) type mfaVerifyFormData struct { - MfaType model.MFAType `schema:"mfaType"` + MFAType model.MFAType `schema:"mfaType"` Code string `schema:"code"` } -func (l *Login) handleMfaVerify(w http.ResponseWriter, r *http.Request) { +func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) { data := new(mfaVerifyFormData) authReq, err := l.getAuthRequestAndParseData(r, data) if err != nil { l.renderError(w, r, authReq, err) return } - if data.MfaType == model.MFATypeOTP { + if data.MFAType == model.MFATypeOTP { userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.authRepo.VerifyMfaOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Code, userAgentID, model.BrowserInfoFromRequest(r)) + err = l.authRepo.VerifyMFAOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Code, userAgentID, model.BrowserInfoFromRequest(r)) } if err != nil { l.renderError(w, r, authReq, err) @@ -34,15 +34,23 @@ func (l *Login) handleMfaVerify(w http.ResponseWriter, r *http.Request) { l.renderNextStep(w, r, authReq) } -func (l *Login) renderMfaVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, verificationStep *model.MfaVerificationStep, err error) { +func (l *Login) renderMFAVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, verificationStep *model.MFAVerificationStep, err error) { var errType, errMessage string if err != nil { errMessage = l.getErrorMessage(r, err) } - data := l.getUserData(r, authReq, "Mfa Verify", errType, errMessage) - if verificationStep != nil { - data.MfaProviders = verificationStep.MfaProviders - data.SelectedMfaProvider = verificationStep.MfaProviders[0] + data := l.getUserData(r, authReq, "MFA Verify", errType, errMessage) + if verificationStep == nil { + l.renderError(w, r, authReq, err) + return } - l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaVerify], data, nil) + switch verificationStep.MFAProviders[len(verificationStep.MFAProviders)-1] { + case model.MFATypeU2F: + l.renderU2FVerification(w, r, authReq, nil) + return + case model.MFATypeOTP: + data.MFAProviders = verificationStep.MFAProviders + data.SelectedMFAProvider = model.MFATypeOTP + } + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMFAVerify], data, nil) } diff --git a/internal/ui/login/handler/mfa_verify_u2f_handler.go b/internal/ui/login/handler/mfa_verify_u2f_handler.go new file mode 100644 index 0000000000..51b734ab0a --- /dev/null +++ b/internal/ui/login/handler/mfa_verify_u2f_handler.go @@ -0,0 +1,59 @@ +package handler + +import ( + "encoding/base64" + "net/http" + + http_mw "github.com/caos/zitadel/internal/api/http/middleware" + "github.com/caos/zitadel/internal/auth_request/model" + user_model "github.com/caos/zitadel/internal/user/model" +) + +const ( + tmplU2FVerification = "u2fverification" +) + +func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { + var errType, errMessage, credentialData string + var webAuthNLogin *user_model.WebAuthNLogin + if err == nil { + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + webAuthNLogin, err = l.authRepo.BeginMFAU2FLogin(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.ID, userAgentID) + } + if err != nil { + errMessage = l.getErrorMessage(r, err) + } + if webAuthNLogin != nil { + credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData) + } + data := &webAuthNData{ + userData: l.getUserData(r, authReq, "Login WebAuthNToken", errType, errMessage), + CredentialCreationData: credentialData, + } + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplU2FVerification], data, nil) +} + +func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) { + formData := new(webAuthNFormData) + authReq, err := l.getAuthRequestAndParseData(r, formData) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if formData.Recreate { + l.renderU2FVerification(w, r, authReq, nil) + return + } + credData, err := base64.URLEncoding.DecodeString(formData.CredentialData) + if err != nil { + l.renderU2FVerification(w, r, authReq, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.VerifyMFAU2F(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.ID, userAgentID, credData, model.BrowserInfoFromRequest(r)) + if err != nil { + l.renderU2FVerification(w, r, authReq, err) + return + } + l.renderNextStep(w, r, authReq) +} diff --git a/internal/ui/login/handler/passwordless_login_handler.go b/internal/ui/login/handler/passwordless_login_handler.go new file mode 100644 index 0000000000..116f0b9cbf --- /dev/null +++ b/internal/ui/login/handler/passwordless_login_handler.go @@ -0,0 +1,59 @@ +package handler + +import ( + "encoding/base64" + "net/http" + + http_mw "github.com/caos/zitadel/internal/api/http/middleware" + "github.com/caos/zitadel/internal/auth_request/model" + user_model "github.com/caos/zitadel/internal/user/model" +) + +const ( + tmplPasswordlessVerification = "passwordlessverification" +) + +func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { + var errType, errMessage, credentialData string + var webAuthNLogin *user_model.WebAuthNLogin + if err == nil { + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + webAuthNLogin, err = l.authRepo.BeginPasswordlessLogin(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.ID, userAgentID) + } + if err != nil { + errMessage = l.getErrorMessage(r, err) + } + if webAuthNLogin != nil { + credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData) + } + data := &webAuthNData{ + userData: l.getUserData(r, authReq, "Login Passwordless", errType, errMessage), + CredentialCreationData: credentialData, + } + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplPasswordlessVerification], data, nil) +} + +func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) { + formData := new(webAuthNFormData) + authReq, err := l.getAuthRequestAndParseData(r, formData) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if formData.Recreate { + l.renderPasswordlessVerification(w, r, authReq, nil) + return + } + credData, err := base64.URLEncoding.DecodeString(formData.CredentialData) + if err != nil { + l.renderPasswordlessVerification(w, r, authReq, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.VerifyPasswordless(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.ID, userAgentID, credData, model.BrowserInfoFromRequest(r)) + if err != nil { + l.renderPasswordlessVerification(w, r, authReq, err) + return + } + l.renderNextStep(w, r, authReq) +} diff --git a/internal/ui/login/handler/renderer.go b/internal/ui/login/handler/renderer.go index 7b1d563551..dad96049cb 100644 --- a/internal/ui/login/handler/renderer.go +++ b/internal/ui/login/handler/renderer.go @@ -3,18 +3,19 @@ package handler import ( "errors" "fmt" - "github.com/caos/logging" - iam_model "github.com/caos/zitadel/internal/iam/model" - "github.com/gorilla/csrf" - "golang.org/x/text/language" "html/template" "net/http" "path" + "github.com/caos/logging" + "github.com/gorilla/csrf" + "golang.org/x/text/language" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/i18n" + iam_model "github.com/caos/zitadel/internal/iam/model" "github.com/caos/zitadel/internal/renderer" ) @@ -32,31 +33,34 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str pathPrefix: pathPrefix, } tmplMapping := map[string]string{ - tmplError: "error.html", - tmplLogin: "login.html", - tmplUserSelection: "select_user.html", - tmplPassword: "password.html", - tmplMfaVerify: "mfa_verify.html", - tmplMfaPrompt: "mfa_prompt.html", - tmplMfaInitVerify: "mfa_init_verify.html", - tmplMfaInitDone: "mfa_init_done.html", - tmplMailVerification: "mail_verification.html", - tmplMailVerified: "mail_verified.html", - tmplInitPassword: "init_password.html", - tmplInitPasswordDone: "init_password_done.html", - tmplInitUser: "init_user.html", - tmplInitUserDone: "init_user_done.html", - tmplPasswordResetDone: "password_reset_done.html", - tmplChangePassword: "change_password.html", - tmplChangePasswordDone: "change_password_done.html", - tmplRegisterOption: "register_option.html", - tmplRegister: "register.html", - tmplLogoutDone: "logout_done.html", - tmplRegisterOrg: "register_org.html", - tmplChangeUsername: "change_username.html", - tmplChangeUsernameDone: "change_username_done.html", - tmplLinkUsersDone: "link_users_done.html", - tmplExternalNotFoundOption: "external_not_found_option.html", + tmplError: "error.html", + tmplLogin: "login.html", + tmplUserSelection: "select_user.html", + tmplPassword: "password.html", + tmplPasswordlessVerification: "passwordless.html", + tmplMFAVerify: "mfa_verify.html", + tmplMFAPrompt: "mfa_prompt.html", + tmplMFAInitVerify: "mfa_init_verify.html", + tmplMFAU2FInit: "mfa_init_u2f.html", + tmplU2FVerification: "mfa_verification_u2f.html", + tmplMFAInitDone: "mfa_init_done.html", + tmplMailVerification: "mail_verification.html", + tmplMailVerified: "mail_verified.html", + tmplInitPassword: "init_password.html", + tmplInitPasswordDone: "init_password_done.html", + tmplInitUser: "init_user.html", + tmplInitUserDone: "init_user_done.html", + tmplPasswordResetDone: "password_reset_done.html", + tmplChangePassword: "change_password.html", + tmplChangePasswordDone: "change_password_done.html", + tmplRegisterOption: "register_option.html", + tmplRegister: "register.html", + tmplLogoutDone: "logout_done.html", + tmplRegisterOrg: "register_org.html", + tmplChangeUsername: "change_username.html", + tmplChangeUsernameDone: "change_username_done.html", + tmplLinkUsersDone: "link_users_done.html", + tmplExternalNotFoundOption: "external_not_found_option.html", } funcs := map[string]interface{}{ "resourceUrl": func(file string) string { @@ -86,6 +90,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str "userSelectionUrl": func() string { return path.Join(r.pathPrefix, EndpointUserSelection) }, + "passwordLessVerificationUrl": func() string { + return path.Join(r.pathPrefix, EndpointPasswordlessLogin) + }, "passwordResetUrl": func(id string) string { return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointPasswordReset, queryAuthRequestID, id)) }, @@ -93,16 +100,22 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str return path.Join(r.pathPrefix, EndpointPassword) }, "mfaVerifyUrl": func() string { - return path.Join(r.pathPrefix, EndpointMfaVerify) + return path.Join(r.pathPrefix, EndpointMFAVerify) }, "mfaPromptUrl": func() string { - return path.Join(r.pathPrefix, EndpointMfaPrompt) + return path.Join(r.pathPrefix, EndpointMFAPrompt) }, "mfaPromptChangeUrl": func(id string, provider model.MFAType) string { - return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s;%s=%v", EndpointMfaPrompt, queryAuthRequestID, id, "provider", provider)) + return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s;%s=%v", EndpointMFAPrompt, queryAuthRequestID, id, "provider", provider)) }, "mfaInitVerifyUrl": func() string { - return path.Join(r.pathPrefix, EndpointMfaInitVerify) + return path.Join(r.pathPrefix, EndpointMFAInitVerify) + }, + "mfaInitU2FVerifyUrl": func() string { + return path.Join(r.pathPrefix, EndpointMFAInitU2FVerify) + }, + "mfaInitU2FLoginUrl": func() string { + return path.Join(r.pathPrefix, EndpointU2FVerification) }, "mailVerificationUrl": func() string { return path.Join(r.pathPrefix, EndpointMailVerification) @@ -190,8 +203,10 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq * l.renderInitPassword(w, r, authReq, authReq.UserID, "", err) case *model.PasswordStep: l.renderPassword(w, r, authReq, nil) - case *model.MfaVerificationStep: - l.renderMfaVerify(w, r, authReq, step, err) + case *model.PasswordlessStep: + l.renderPasswordlessVerification(w, r, authReq, nil) + case *model.MFAVerificationStep: + l.renderMFAVerify(w, r, authReq, step, err) case *model.RedirectToCallbackStep: if len(authReq.PossibleSteps) > 1 { l.chooseNextStep(w, r, authReq, 1, err) @@ -202,8 +217,8 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq * l.renderChangePassword(w, r, authReq, err) case *model.VerifyEMailStep: l.renderMailVerification(w, r, authReq, "", err) - case *model.MfaPromptStep: - l.renderMfaPrompt(w, r, authReq, step, err) + case *model.MFAPromptStep: + l.renderMFAPrompt(w, r, authReq, step, err) case *model.InitUserStep: l.renderInitUser(w, r, authReq, "", "", step.PasswordSet, nil) case *model.ChangeUsernameStep: @@ -356,8 +371,8 @@ type userData struct { baseData profileData PasswordChecked string - MfaProviders []model.MFAType - SelectedMfaProvider model.MFAType + MFAProviders []model.MFAType + SelectedMFAProvider model.MFAType Linking bool } @@ -386,21 +401,21 @@ type userSelectionData struct { type mfaData struct { baseData profileData - MfaProviders []model.MFAType - MfaRequired bool + MFAProviders []model.MFAType + MFARequired bool } type mfaVerifyData struct { baseData profileData - MfaType model.MFAType + MFAType model.MFAType otpData } type mfaDoneData struct { baseData profileData - MfaType model.MFAType + MFAType model.MFAType } type otpData struct { diff --git a/internal/ui/login/handler/router.go b/internal/ui/login/handler/router.go index 7b1651b9eb..6d3b8cde36 100644 --- a/internal/ui/login/handler/router.go +++ b/internal/ui/login/handler/router.go @@ -13,6 +13,7 @@ const ( EndpointLogin = "/login" EndpointExternalLogin = "/login/externalidp" EndpointExternalLoginCallback = "/login/externalidp/callback" + EndpointPasswordlessLogin = "/login/passwordless" EndpointLoginName = "/loginname" EndpointUserSelection = "/userselection" EndpointChangeUsername = "/username/change" @@ -21,9 +22,11 @@ const ( EndpointChangePassword = "/password/change" EndpointPasswordReset = "/password/reset" EndpointInitUser = "/user/init" - EndpointMfaVerify = "/mfa/verify" - EndpointMfaPrompt = "/mfa/prompt" - EndpointMfaInitVerify = "/mfa/init/verify" + EndpointMFAVerify = "/mfa/verify" + EndpointMFAPrompt = "/mfa/prompt" + EndpointMFAInitVerify = "/mfa/init/verify" + EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify" + EndpointU2FVerification = "/mfa/u2f/verify" EndpointMailVerification = "/mail/verification" EndpointMailVerified = "/mail/verified" EndpointRegisterOption = "/register/option" @@ -46,6 +49,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost) router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) + router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost) router.HandleFunc(EndpointLoginName, login.handleLoginName).Methods(http.MethodGet) router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost) router.HandleFunc(EndpointUserSelection, login.handleSelectUser).Methods(http.MethodPost) @@ -56,10 +60,12 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointPasswordReset, login.handlePasswordReset).Methods(http.MethodGet) router.HandleFunc(EndpointInitUser, login.handleInitUser).Methods(http.MethodGet) router.HandleFunc(EndpointInitUser, login.handleInitUserCheck).Methods(http.MethodPost) - router.HandleFunc(EndpointMfaVerify, login.handleMfaVerify).Methods(http.MethodPost) - router.HandleFunc(EndpointMfaPrompt, login.handleMfaPromptSelection).Methods(http.MethodGet) - router.HandleFunc(EndpointMfaPrompt, login.handleMfaPrompt).Methods(http.MethodPost) - router.HandleFunc(EndpointMfaInitVerify, login.handleMfaInitVerify).Methods(http.MethodPost) + router.HandleFunc(EndpointMFAVerify, login.handleMFAVerify).Methods(http.MethodPost) + router.HandleFunc(EndpointMFAPrompt, login.handleMFAPromptSelection).Methods(http.MethodGet) + router.HandleFunc(EndpointMFAPrompt, login.handleMFAPrompt).Methods(http.MethodPost) + router.HandleFunc(EndpointMFAInitVerify, login.handleMFAInitVerify).Methods(http.MethodPost) + router.HandleFunc(EndpointMFAInitU2FVerify, login.handleRegisterU2F).Methods(http.MethodPost) + router.HandleFunc(EndpointU2FVerification, login.handleU2FVerification).Methods(http.MethodPost) router.HandleFunc(EndpointMailVerification, login.handleMailVerification).Methods(http.MethodGet) router.HandleFunc(EndpointMailVerification, login.handleMailVerificationCheck).Methods(http.MethodPost) router.HandleFunc(EndpointChangePassword, login.handleChangePassword).Methods(http.MethodPost) diff --git a/internal/ui/login/handler/webauthn.go b/internal/ui/login/handler/webauthn.go new file mode 100644 index 0000000000..83ade70060 --- /dev/null +++ b/internal/ui/login/handler/webauthn.go @@ -0,0 +1,12 @@ +package handler + +type webAuthNData struct { + userData + CredentialCreationData string +} + +type webAuthNFormData struct { + CredentialData string `schema:"credentialData"` + Name string `schema:"name"` + Recreate bool `schema:"recreate"` +} diff --git a/internal/ui/login/static/i18n/de.yaml b/internal/ui/login/static/i18n/de.yaml index 0954839834..165d5eb443 100644 --- a/internal/ui/login/static/i18n/de.yaml +++ b/internal/ui/login/static/i18n/de.yaml @@ -35,10 +35,10 @@ UsernameChangeDone: Title: Username geändert Description: Der Username wurde erfolgreich geändert. -MfaVerify: +MFAVerify: Title: Multifaktor verifizieren Description: Verifiziere deinen Multifaktor - OTP: OTP + OTP: OTP (One Time Password) Code: Code InitPassword: @@ -63,23 +63,41 @@ InitUserDone: Title: User aktiviert Description: EMail verifiziert und Passwort erfolgreich gesetzt -MfaPrompt: +MFAPrompt: Title: Multifaktor hinzufügen Description: Möchtest du einen Mulitfaktor hinzufügen? Provider0: OTP (One Time Password) Provider1: U2F (Universal 2nd Factor) -MfaInitVerify: +MFAInitVerify: Title: Multifaktor Verifizierung Description: Verifiziere deinen Multifaktor - OtpDescription: Scanne den Code mit einem Authentifizierungs-App (z.B Google Authentificator) oder kopiere das Secret und gib anschliessend den Code ein. + OTPDescription: Scanne den Code mit einem Authentifizierungs-App (z.B Google Authenticator) oder kopiere das Secret und gib anschliessend den Code ein. Secret: Secret Code: Code -MfaInitDone: +MFAInitDone: Title: Multifaktor Verifizierung erstellt Description: Multifikator Verifizierung erfolgreich abgeschlossen. Der Multifaktor muss bei jeder Anmeldung eingegeben werden, dies beinhaltet auch den aktuellen Authentifizierungs Prozess. +MFAInitU2F: + Title: Multifaktor U2F / WebAuthN hinzufügen + Description: Füge dein Token hinzu, indem du einen Namen eingibst und den 'Token registrieren' Button drückst. + +MFAVerifyU2F: + Title: Multifaktor Verifizierung + Description: Verifiziere deinen Multifaktor U2F / WebAuthN Token + +WebAuthN: + Name: Name des Tokens / Geräts + NotSupported: WebAuthN wird durch deinen Browser nicht unterstützt. Stelle sicher, dass du die aktuelle Version installiert hast oder nutze einen anderen (z.B. Chrome, Safari, Firefox) + Error: + Retry: Versuche es erneut, erstelle eine neue Abfrage oder wähle einen andere Methode. + +Passwordless: + Title: Passwortlos einloggen + Description: Verifiziere dein Token + PasswordChange: Title: Passwort ändern Description: Ändere dein Password in dem du dein altes und dann dein neuen Passwort eingibst. @@ -181,6 +199,9 @@ Actions: ForgotPassword: Password zurücksetzen Cancel: Abbrechen Save: speichern + RegisterToken: Token registrieren + ValidateToken: Token validieren + Recreate: erneut erstellen Errors: Internal: Es ist ein interner Fehler aufgetreten @@ -215,9 +236,9 @@ Errors: GeneratorAlgNotSupported: Generator Algorithums wird nicht unterstützt EmailVerify: UserIDEmpty: UserID ist leer - Mfa: + MFA: NoProviders: Es stehen keine Multifaktorprovider zur Verfügung - Otp: + OTP: AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht InvalidCode: Code ist ungültig diff --git a/internal/ui/login/static/i18n/en.yaml b/internal/ui/login/static/i18n/en.yaml index ffaff11d73..f1c0d1f555 100644 --- a/internal/ui/login/static/i18n/en.yaml +++ b/internal/ui/login/static/i18n/en.yaml @@ -35,10 +35,10 @@ UsernameChangeDone: Title: Username changed Description: Your username was changed successfully. -MfaVerify: +MFAVerify: Title: Verify Multificator Description: Verify your multifactor - OTP: OTP + OTP: OTP (One Time Password) Code: Code InitPassword: @@ -63,23 +63,41 @@ InitUserDone: Title: User activated Description: Email verified and Password successfully set -MfaPrompt: +MFAPrompt: Title: Multifactor Setup Description: Would you like to setup multifactor authentication? Provider0: OTP (One Time Password) Provider1: U2F (Universal 2nd Factor) -MfaInitVerify: +MFAInitVerify: Title: Multifactor Verification Description: Verify your multifactor. - OtpDescription: Scan the code with your authenticator app (e.g Google-Authenticator) or copy the secret and insert the generated code below. + OTPDescription: Scan the code with your authenticator app (e.g Google Authenticator) or copy the secret and insert the generated code below. Secret: Secret Code: Code -MfaInitDone: +MFAInitDone: Title: Multifcator Verification done Description: Multifactor verification successfully done. The multifactor has to be entered on each login, even in the actual authentification process. +MFAInitU2F: + Title: Multifactor Setup U2F / WebAuthN + Description: Add your Token by providing a name and then clicking on the 'Register Token' button below. + +MFAVerifyU2F: + Title: Multifactor Verification + Description: Verify your multifactor U2F / WebAuthN token + +WebAuthN: + Name: Name of the tokens / machine + NotSupported: WebAuthN is not supported by your browser. Please ensure it is up to date or use a different one (e.g. Chrome, Safari, Firefox) + Error: + Retry: Retry, create a new challenge or choose a different method. + +Passwordless: + Title: Login passwordles + Description: Verify your token + PasswordChange: Title: Change Password Description: Change your password. Enter your old and new password. @@ -181,6 +199,9 @@ Actions: ForgotPassword: reset password Cancel: cancel Save: save + RegisterToken: Register Token + ValidateToken: Validate Token + Recreate: recreate Errors: Internal: An internal error occured @@ -215,9 +236,9 @@ Errors: GeneratorAlgNotSupported: Unsupported generator algorithm EmailVerify: UserIDEmpty: UserID is empty - Mfa: + MFA: NoProviders: No available multifactor providers - Otp: + OTP: AlreadyReady: Multifactor OTP (OneTimePassword) is already setup NotExisting: Multifactor OTP (OneTimePassword) doesn't exist InvalidCode: Invalid code diff --git a/internal/ui/login/static/resources/scripts/base64.js b/internal/ui/login/static/resources/scripts/base64.js new file mode 100644 index 0000000000..075fe9d48d --- /dev/null +++ b/internal/ui/login/static/resources/scripts/base64.js @@ -0,0 +1,68 @@ +/* + * modified version of: + * + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ + +"use strict"; + +let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +// Use a lookup table to find the index. +let lookup = new Uint8Array(256); +for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; +} + +function encode(arraybuffer) { + let bytes = new Uint8Array(arraybuffer), + i, len = bytes.length, base64 = ""; + + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + + if ((len % 3) === 2) { + base64 = base64.substring(0, base64.length - 1) + "="; + } else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + "=="; + } + + return base64; +} + +function decode(base64) { + let bufferLength = base64.length * 0.75, + len = base64.length, i, p = 0, + encoded1, encoded2, encoded3, encoded4; + + if (base64[base64.length - 1] === "=") { + bufferLength--; + if (base64[base64.length - 2] === "=") { + bufferLength--; + } + } + + let arraybuffer = new ArrayBuffer(bufferLength), + bytes = new Uint8Array(arraybuffer); + + for (i = 0; i < len; i += 4) { + encoded1 = lookup[base64.charCodeAt(i)]; + encoded2 = lookup[base64.charCodeAt(i + 1)]; + encoded3 = lookup[base64.charCodeAt(i + 2)]; + encoded4 = lookup[base64.charCodeAt(i + 3)]; + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; +} \ No newline at end of file diff --git a/internal/ui/login/static/resources/scripts/webauthn.js b/internal/ui/login/static/resources/scripts/webauthn.js new file mode 100644 index 0000000000..74c54c4db5 --- /dev/null +++ b/internal/ui/login/static/resources/scripts/webauthn.js @@ -0,0 +1,31 @@ +function checkWebauthnSupported(button, func) { + let support = document.getElementsByClassName("wa-support"); + let noSupport = document.getElementsByClassName("wa-no-support"); + if (typeof (PublicKeyCredential) === undefined) { + for (let item of noSupport) { + item.classList.remove('hidden'); + } + for (let item of support) { + item.classList.add('hidden'); + } + return + } + document.getElementById(button).addEventListener('click', func); +} + +function webauthnError(error) { + let err = document.getElementById('wa-error'); + err.getElementsByClassName('cause')[0].innerText = error.message; + err.classList.remove('hidden'); +} + +function bufferDecode(value) { + return decode(value); +} + +function bufferEncode(value) { + return encode(value) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} diff --git a/internal/ui/login/static/resources/scripts/webauthn_login.js b/internal/ui/login/static/resources/scripts/webauthn_login.js new file mode 100644 index 0000000000..a9264ffe1f --- /dev/null +++ b/internal/ui/login/static/resources/scripts/webauthn_login.js @@ -0,0 +1,42 @@ +document.addEventListener('DOMContentLoaded', checkWebauthnSupported('btn-login', login)); + +function login() { + document.getElementById('wa-error').classList.add('hidden'); + + let makeAssertionOptions = JSON.parse(atob(document.getElementsByName('credentialAssertionData')[0].value)); + makeAssertionOptions.publicKey.challenge = bufferDecode(makeAssertionOptions.publicKey.challenge); + makeAssertionOptions.publicKey.allowCredentials.forEach(function (listItem) { + listItem.id = bufferDecode(listItem.id) + }); + console.log(makeAssertionOptions); + navigator.credentials.get({ + publicKey: makeAssertionOptions.publicKey + }).then(function (credential) { + verifyAssertion(credential); + }).catch(function (err) { + webauthnError(err); + }); +} + +function verifyAssertion(assertedCredential) { + let authData = new Uint8Array(assertedCredential.response.authenticatorData); + let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); + let rawId = new Uint8Array(assertedCredential.rawId); + let sig = new Uint8Array(assertedCredential.response.signature); + let userHandle = new Uint8Array(assertedCredential.response.userHandle); + + let data = JSON.stringify({ + id: assertedCredential.id, + rawId: bufferEncode(rawId), + type: assertedCredential.type, + response: { + authenticatorData: bufferEncode(authData), + clientDataJSON: bufferEncode(clientDataJSON), + signature: bufferEncode(sig), + userHandle: bufferEncode(userHandle), + }, + }) + + document.getElementsByName('credentialData')[0].value = btoa(data); + document.getElementsByTagName('form')[0].submit(); +} \ No newline at end of file diff --git a/internal/ui/login/static/resources/scripts/webauthn_register.js b/internal/ui/login/static/resources/scripts/webauthn_register.js new file mode 100644 index 0000000000..153f6f1abf --- /dev/null +++ b/internal/ui/login/static/resources/scripts/webauthn_register.js @@ -0,0 +1,42 @@ +document.addEventListener('DOMContentLoaded', checkWebauthnSupported('btn-register', registerCredential)); + +function registerCredential() { + document.getElementById('wa-error').classList.add('hidden'); + + let opt = JSON.parse(atob(document.getElementsByName('credentialCreationData')[0].value)); + opt.publicKey.challenge = bufferDecode(opt.publicKey.challenge); + opt.publicKey.user.id = bufferDecode(opt.publicKey.user.id); + if (opt.publicKey.excludeCredentials) { + for (let i = 0; i < opt.publicKey.excludeCredentials.length; i++) { + if (opt.publicKey.excludeCredentials[i].id !== null) { + opt.publicKey.excludeCredentials[i].id = bufferDecode(opt.publicKey.excludeCredentials[i].id); + } + } + } + navigator.credentials.create({ + publicKey: opt.publicKey + }).then(function (credential) { + createCredential(credential); + }).catch(function (err) { + webauthnError(err); + }); +} + +function createCredential(newCredential) { + let attestationObject = new Uint8Array(newCredential.response.attestationObject); + let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); + let rawId = new Uint8Array(newCredential.rawId); + + let data = JSON.stringify({ + id: newCredential.id, + rawId: bufferEncode(rawId), + type: newCredential.type, + response: { + attestationObject: bufferEncode(attestationObject), + clientDataJSON: bufferEncode(clientDataJSON), + }, + }); + + document.getElementsByName('credentialData')[0].value = btoa(data); + document.getElementsByTagName('form')[0].submit(); +} \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/caos/css/dark.css b/internal/ui/login/static/resources/themes/caos/css/dark.css index 80a79a848a..d8fd885a54 100644 --- a/internal/ui/login/static/resources/themes/caos/css/dark.css +++ b/internal/ui/login/static/resources/themes/caos/css/dark.css @@ -487,4 +487,12 @@ footer { color: #F20D6B; } +.hidden { + display: none; +} + +#wa-error { + margin-top: 20px; +} + /*# sourceMappingURL=dark.css.map */ diff --git a/internal/ui/login/static/resources/themes/caos/css/dark.css.map b/internal/ui/login/static/resources/themes/caos/css/dark.css.map index 449a4e6fe6..5890330f09 100644 --- a/internal/ui/login/static/resources/themes/caos/css/dark.css.map +++ b/internal/ui/login/static/resources/themes/caos/css/dark.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;EF0BT;;;AAGJ;EACI,OCnBQ;EDoBR,aC1BS;ED2BT;EACA,WEhCU;;;AFmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvDW;EDwDX;EACA;;AAEA;EACI,OC3DY;;AD8DhB;EACI;;;AAIR;EACI,kBCvEc;EDwEd,OCtEW;EDuEX;EACA;EACA;EACA;EACA,QExFU;EFyFV;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBCpFY;EDqFZ,OCxFU;EDyFV;;AAGJ;EACI,kBC3FO;ED4FP,OC7FI;ED8FJ;;AACA;EACI,kBC9FQ;;ADkGhB;EACI,kBExFW;EFyFX;;AAEA;EACI,kBE5FO;EF6FP;;AAIR;EACI;EACA;EACA;EACA;EACA,OEhGa;EFiGb,kBEhGmB;;AFkGnB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;;AAOZ;EACI,kBE9HmB;EF+HnB,OC7IQ;ED8IR,QE1JU;EF2JV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EEzJN;;AACA;EFoJE;IEnJA;IACA;;;AF0JA;EE7JF;;AACA;EF4JE;IE3JA;IACA;;;;AFiKA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WEvLE;EFwLF;;AAGJ;EACI;EACA;EACA;EACA,OE1KC;;;AFgLT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OC/NA;;ADmOR;EACI,OExNK;EFyNL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OC9PI;ED+PJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBExPW;;AF2Pf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cEjRO;EFkRP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OE7SP;;AFoTL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EEvUV;;AACA;EFkUM;IEjUJ;IACA;;;AFyUQ;EACI;EACA;EE9Ud;;AACA;EF2UU;IE1UR;IACA;;;AFgVI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OErVN;;AF0VE;EACI,OE5VL;;AFiWP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MCxYI;;AD2YR;EACI,MC7YU;;;ADkZd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OE5aO","file":"dark.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;EF0BT;;;AAGJ;EACI,OCnBQ;EDoBR,aC1BS;ED2BT;EACA,WEhCU;;;AFmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvDW;EDwDX;EACA;;AAEA;EACI,OC3DY;;AD8DhB;EACI;;;AAIR;EACI,kBCvEc;EDwEd,OCtEW;EDuEX;EACA;EACA;EACA;EACA,QExFU;EFyFV;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBCpFY;EDqFZ,OCxFU;EDyFV;;AAGJ;EACI,kBC3FO;ED4FP,OC7FI;ED8FJ;;AACA;EACI,kBC9FQ;;ADkGhB;EACI,kBExFW;EFyFX;;AAEA;EACI,kBE5FO;EF6FP;;AAIR;EACI;EACA;EACA;EACA;EACA,OEhGa;EFiGb,kBEhGmB;;AFkGnB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;;AAOZ;EACI,kBE9HmB;EF+HnB,OC7IQ;ED8IR,QE1JU;EF2JV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EEzJN;;AACA;EFoJE;IEnJA;IACA;;;AF0JA;EE7JF;;AACA;EF4JE;IE3JA;IACA;;;;AFiKA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WEvLE;EFwLF;;AAGJ;EACI;EACA;EACA;EACA,OE1KC;;;AFgLT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OC/NA;;ADmOR;EACI,OExNK;EFyNL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OC9PI;ED+PJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBExPW;;AF2Pf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cEjRO;EFkRP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OE7SP;;AFoTL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EEvUV;;AACA;EFkUM;IEjUJ;IACA;;;AFyUQ;EACI;EACA;EE9Ud;;AACA;EF2UU;IE1UR;IACA;;;AFgVI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OErVN;;AF0VE;EACI,OE5VL;;AFiWP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MCxYI;;AD2YR;EACI,MC7YU;;;ADkZd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OE5aO;;;AF+aX;EACI;;;AAGJ;EACI","file":"dark.css"} \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/caos/css/light.css b/internal/ui/login/static/resources/themes/caos/css/light.css index e9168da6da..ed2241a8fe 100644 --- a/internal/ui/login/static/resources/themes/caos/css/light.css +++ b/internal/ui/login/static/resources/themes/caos/css/light.css @@ -487,6 +487,14 @@ footer { color: #F20D6B; } +.hidden { + display: none; +} + +#wa-error { + margin-top: 20px; +} + html { background-color: white; color: #282828; diff --git a/internal/ui/login/static/resources/themes/caos/css/light.css.map b/internal/ui/login/static/resources/themes/caos/css/light.css.map index aeb7aefcf1..6eb1e80ecf 100644 --- a/internal/ui/login/static/resources/themes/caos/css/light.css.map +++ b/internal/ui/login/static/resources/themes/caos/css/light.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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","file":"light.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;EF0BT;;;AAGJ;EACI,OCnBQ;EDoBR,aC1BS;ED2BT;EACA,WEhCU;;;AFmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvDW;EDwDX;EACA;;AAEA;EACI,OC3DY;;AD8DhB;EACI;;;AAIR;EACI,kBCvEc;EDwEd,OCtEW;EDuEX;EACA;EACA;EACA;EACA,QExFU;EFyFV;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBCpFY;EDqFZ,OCxFU;EDyFV;;AAGJ;EACI,kBC3FO;ED4FP,OC7FI;ED8FJ;;AACA;EACI,kBC9FQ;;ADkGhB;EACI,kBExFW;EFyFX;;AAEA;EACI,kBE5FO;EF6FP;;AAIR;EACI;EACA;EACA;EACA;EACA,OEhGa;EFiGb,kBEhGmB;;AFkGnB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;;AAOZ;EACI,kBE9HmB;EF+HnB,OC7IQ;ED8IR,QE1JU;EF2JV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EEzJN;;AACA;EFoJE;IEnJA;IACA;;;AF0JA;EE7JF;;AACA;EF4JE;IE3JA;IACA;;;;AFiKA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WEvLE;EFwLF;;AAGJ;EACI;EACA;EACA;EACA,OE1KC;;;AFgLT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OC/NA;;ADmOR;EACI,OExNK;EFyNL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OC9PI;ED+PJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBExPW;;AF2Pf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cEjRO;EFkRP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OE7SP;;AFoTL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EEvUV;;AACA;EFkUM;IEjUJ;IACA;;;AFyUQ;EACI;EACA;EE9Ud;;AACA;EF2UU;IE1UR;IACA;;;AFgVI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OErVN;;AF0VE;EACI,OE5VL;;AFiWP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MCxYI;;AD2YR;EACI,MC7YU;;;ADkZd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OE5aO;;;AF+aX;EACI;;;AAGJ;EACI;;;AGzdJ;EACI,kBFeQ;EEdR,OFac;;AERd;EACI;;AAGJ;EACI,OFGU;;AEAd;EACI;EACA;EACA;;AAEA;EACI,kBFIa;EEHb;EACA,ODyBgB;;ACtBpB;EACI,kBFVG;EEWH,ODoBgB;ECnBhB;EACA;;AACA;EACI,kBFdI;;AEkBZ;EACI,kBDRO;ECSP;;AAEA;EACI,kBDZG;ECaH;;AAIR;EACI,OFhCM;;AEkCN;EACI;EACA,kBDHY;;ACQhB;EDxCV;;AACA;ECuCU;IDtCR;IACA;;;ACyCQ;EACI,kBDbY;;ACeZ;ED/Cd;;AACA;EC8Cc;ID7CZ;IACA;;;ACmDQ;EDtDV;;AACA;ECqDU;IDpDR;IACA;;;ACwDY;ED3Dd;;AACA;EC0Dc;IDzDZ;IACA;;;AC8DI;EACI,OD7Bc;EC8Bd,kBD7BoB;;AC+BpB;EACI;;AAKZ;EACI,kBD5CoB;EC6CpB,OF9EU;;AEkFV;EACI,MFnFM;;AEsFV;EACI,MFtFA;;AE0FR;EAEQ;;;AAMR;EACI,OFpGU;;AEwGb;EACI,ODhEM;;ACoEN;EACI,ODtEG;;;AC8EZ;EDrHF;;AACA;ECoHE;IDnHA;IACA;;;ACsHA;EDzHF;;AACA;ECwHE;IDvHA;IACA;;;;AC2HJ;EACI;;;AAGJ;EACI,OD5FY","file":"light.css"} \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/scss/main.scss b/internal/ui/login/static/resources/themes/scss/main.scss index 2a43896f99..9fb1bf4511 100644 --- a/internal/ui/login/static/resources/themes/scss/main.scss +++ b/internal/ui/login/static/resources/themes/scss/main.scss @@ -466,3 +466,11 @@ footer { .error { color: $nokColor; } + +.hidden { + display: none; +} + +#wa-error { + margin-top: 20px; +} diff --git a/internal/ui/login/static/resources/themes/zitadel/css/dark.css b/internal/ui/login/static/resources/themes/zitadel/css/dark.css index 87f37afe9b..f32c2bc0e0 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/dark.css +++ b/internal/ui/login/static/resources/themes/zitadel/css/dark.css @@ -487,4 +487,12 @@ footer { color: #F20D6B; } +.hidden { + display: none; +} + +#wa-error { + margin-top: 20px; +} + /*# sourceMappingURL=dark.css.map */ diff --git a/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map b/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map index 6bbf67f0ed..3c47167879 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map +++ b/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCQc;EDPd,OCQQ;EDPR;EACA;EACA;EAEI;;;AAIR;EACI,OCFQ;EDGR,aC3BS;ED4BT;EACA,WCzBS;ED0BT;;;AAGJ;EACI,OCVQ;EDWR,aCnCS;EDoCT;EACA,WChCU;;;ADmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OC9CW;ED+CX;EACA;;AAEA;EACI,OClDY;;ADqDhB;EACI;;;AAIR;EACI,kBC9Dc;ED+Dd,OC7DW;ED8DX;EACA;EACA;EACA;EACA,QCxFU;EDyFV;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBC3EY;ED4EZ,OC/EU;EDgFV;;AAGJ;EACI,kBClFO;EDmFP,OCpFI;EDqFJ;;AACA;EACI,kBCrFQ;;ADyFhB;EACI,kBCxFW;EDyFX;;AAEA;EACI,kBC5FO;ED6FP;;AAIR;EACI;EACA;EACA;EACA;EACA,OChGa;EDiGb,kBChGmB;;ADkGnB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;;AAOZ;EACI,kBC9HmB;ED+HnB,OCpIQ;EDqIR,QC1JU;ED2JV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;ECzJN;;AACA;EDoJE;ICnJA;IACA;;;AD0JA;EC7JF;;AACA;ED4JE;IC3JA;IACA;;;;ADiKA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WCvLE;EDwLF;;AAGJ;EACI;EACA;EACA;EACA,OC1KC;;;ADgLT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OCtNA;;AD0NR;EACI,OCxNK;EDyNL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCrPI;EDsPJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBCxPW;;AD2Pf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cCjRO;EDkRP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OC7SP;;ADoTL;EACI;;AAEJ;EACI;EACA;EACA;EACA;ECvUV;;AACA;EDkUM;ICjUJ;IACA;;;ADyUQ;EACI;EACA;EC9Ud;;AACA;ED2UU;IC1UR;IACA;;;ADgVI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OCrVN;;AD0VE;EACI,OC5VL;;ADiWP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC/XI;;ADkYR;EACI,MCpYU;;;ADyYd;EACI;EACA;;;AAIR;EAII;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OC5aO","file":"dark.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCQc;EDPd,OCQQ;EDPR;EACA;EACA;EAEI;;;AAIR;EACI,OCFQ;EDGR,aC3BS;ED4BT;EACA,WCzBS;ED0BT;;;AAGJ;EACI,OCVQ;EDWR,aCnCS;EDoCT;EACA,WChCU;;;ADmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OC9CW;ED+CX;EACA;;AAEA;EACI,OClDY;;ADqDhB;EACI;;;AAIR;EACI,kBC9Dc;ED+Dd,OC7DW;ED8DX;EACA;EACA;EACA;EACA,QCxFU;EDyFV;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBC3EY;ED4EZ,OC/EU;EDgFV;;AAGJ;EACI,kBClFO;EDmFP,OCpFI;EDqFJ;;AACA;EACI,kBCrFQ;;ADyFhB;EACI,kBCxFW;EDyFX;;AAEA;EACI,kBC5FO;ED6FP;;AAIR;EACI;EACA;EACA;EACA;EACA,OChGa;EDiGb,kBChGmB;;ADkGnB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;;AAOZ;EACI,kBC9HmB;ED+HnB,OCpIQ;EDqIR,QC1JU;ED2JV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;ECzJN;;AACA;EDoJE;ICnJA;IACA;;;AD0JA;EC7JF;;AACA;ED4JE;IC3JA;IACA;;;;ADiKA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WCvLE;EDwLF;;AAGJ;EACI;EACA;EACA;EACA,OC1KC;;;ADgLT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OCtNA;;AD0NR;EACI,OCxNK;EDyNL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCrPI;EDsPJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBCxPW;;AD2Pf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cCjRO;EDkRP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OC7SP;;ADoTL;EACI;;AAEJ;EACI;EACA;EACA;EACA;ECvUV;;AACA;EDkUM;ICjUJ;IACA;;;ADyUQ;EACI;EACA;EC9Ud;;AACA;ED2UU;IC1UR;IACA;;;ADgVI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OCrVN;;AD0VE;EACI,OC5VL;;ADiWP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC/XI;;ADkYR;EACI,MCpYU;;;ADyYd;EACI;EACA;;;AAIR;EAII;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OC5aO;;;AD+aX;EACI;;;AAGJ;EACI","file":"dark.css"} \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/zitadel/css/light.css b/internal/ui/login/static/resources/themes/zitadel/css/light.css index 4890c85e1b..6d23b14ad5 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/light.css +++ b/internal/ui/login/static/resources/themes/zitadel/css/light.css @@ -487,6 +487,14 @@ footer { color: #F20D6B; } +.hidden { + display: none; +} + +#wa-error { + margin-top: 20px; +} + html { background-color: #f5f5f5; color: #282828; diff --git a/internal/ui/login/static/resources/themes/zitadel/css/light.css.map b/internal/ui/login/static/resources/themes/zitadel/css/light.css.map index 0383c7d816..28d354b79b 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/light.css.map +++ b/internal/ui/login/static/resources/themes/zitadel/css/light.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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D0Ba;ECzBb;EACA,ODyBgB;;ACtBpB;EACI,kBDDG;ECEH,ODoBgB;ECnBhB;EACA;;AACA;EACI,kBDLI;;ACSZ;EACI,kBDRO;ECSP;;AAEA;EACI,kBDZG;ECaH;;AAIR;EACI,ODvBM;;ACyBN;EACI;EACA,kBDHY;;ACQhB;EDxCV;;AACA;ECuCU;IDtCR;IACA;;;ACyCQ;EACI,kBDbY;;ACeZ;ED/Cd;;AACA;EC8Cc;ID7CZ;IACA;;;ACmDQ;EDtDV;;AACA;ECqDU;IDpDR;IACA;;;ACwDY;ED3Dd;;AACA;EC0Dc;IDzDZ;IACA;;;AC8DI;EACI,OD7Bc;EC8Bd,kBD7BoB;;AC+BpB;EACI;;AAKZ;EACI,kBD5CoB;EC6CpB,ODrEU;;ACyEV;EACI,MD1EM;;AC6EV;EACI,MD1DW;;ACsEnB;EACI,OD3FU;;AC+Fb;EACI,ODhEM;;ACoEN;EACI,ODtEG;;;AC8EZ;EDrHF;;AACA;ECoHE;IDnHA;IACA;;;ACsHA;EDzHF;;AACA;ECwHE;IDvHA;IACA;;;;AC2HJ;EACI;;;AAGJ;EACI,OD5FY","file":"light.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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f,ODSO;ECRP;;AAEA;EACI,kBD0Ba;ECzBb;EACA,ODyBgB;;ACtBpB;EACI,kBDDG;ECEH,ODoBgB;ECnBhB;EACA;;AACA;EACI,kBDLI;;ACSZ;EACI,kBDRO;ECSP;;AAEA;EACI,kBDZG;ECaH;;AAIR;EACI,ODvBM;;ACyBN;EACI;EACA,kBDHY;;ACQhB;EDxCV;;AACA;ECuCU;IDtCR;IACA;;;ACyCQ;EACI,kBDbY;;ACeZ;ED/Cd;;AACA;EC8Cc;ID7CZ;IACA;;;ACmDQ;EDtDV;;AACA;ECqDU;IDpDR;IACA;;;ACwDY;ED3Dd;;AACA;EC0Dc;IDzDZ;IACA;;;AC8DI;EACI,OD7Bc;EC8Bd,kBD7BoB;;AC+BpB;EACI;;AAKZ;EACI,kBD5CoB;EC6CpB,ODrEU;;ACyEV;EACI,MD1EM;;AC6EV;EACI,MD1DW;;ACsEnB;EACI,OD3FU;;AC+Fb;EACI,ODhEM;;ACoEN;EACI,ODtEG;;;AC8EZ;EDrHF;;AACA;ECoHE;IDnHA;IACA;;;ACsHA;EDzHF;;AACA;ECwHE;IDvHA;IACA;;;;AC2HJ;EACI;;;AAGJ;EACI,OD5FY","file":"light.css"} \ No newline at end of file diff --git a/internal/ui/login/static/templates/mfa_init_done.html b/internal/ui/login/static/templates/mfa_init_done.html index 0a233c35ba..7b171e6848 100644 --- a/internal/ui/login/static/templates/mfa_init_done.html +++ b/internal/ui/login/static/templates/mfa_init_done.html @@ -3,7 +3,7 @@
{{t "MfaInitDone.Description"}}
+{{t "MFAInitDone.Description"}}