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":"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;;;ACrCX;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 +{"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":"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;;;ACrCX;EACI,kBD2CmB;EC1CnB,ODsBc;ECpBV;;AAGJ;EACI;;AAGJ;EACI,ODYU;;ACTd;EACI,kBD4Be;EC3Bf,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 +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.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,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;;;AEzdJ;EACI,kBD2CmB;EC1CnB,ODsBc;ECpBV;;AAGJ;EACI;;AAGJ;EACI,ODYU;;ACTd;EACI,kBD4Be;EC3Bf,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 @@
{{ template "user-profile" . }} -

{{t "MfaInitDone.Description"}}

+

{{t "MFAInitDone.Description"}}

@@ -11,7 +11,7 @@ {{ .CSRF }} - +
diff --git a/internal/ui/login/static/templates/mfa_init_u2f.html b/internal/ui/login/static/templates/mfa_init_u2f.html new file mode 100644 index 0000000000..f9f7de78c6 --- /dev/null +++ b/internal/ui/login/static/templates/mfa_init_u2f.html @@ -0,0 +1,42 @@ +{{template "main-top" .}} + +
+ {{ template "user-profile" . }} + +

{{t "MFAInitU2F.Description"}}

+
+ + + + {{ .CSRF }} + + + + + +
+ +
+ + +
+ + +
+ + {{ template "error-message" .}} + +
+ +
+ + + + + + +{{template "main-bottom" .}} + \ No newline at end of file diff --git a/internal/ui/login/static/templates/mfa_init_verify.html b/internal/ui/login/static/templates/mfa_init_verify.html index ab70e39fe6..f4f8839737 100644 --- a/internal/ui/login/static/templates/mfa_init_verify.html +++ b/internal/ui/login/static/templates/mfa_init_verify.html @@ -3,7 +3,7 @@
{{ template "user-profile" . }} -

{{t "MfaInitVerify.Description"}}

+

{{t "MFAInitVerify.Description"}}

@@ -11,25 +11,25 @@ {{ .CSRF }} - + - {{if (eq .MfaType 0) }} -

{{t "MfaInitVerify.OtpDescription"}}

+ {{if (eq .MFAType 0) }} +

{{t "MFAInitVerify.OTPDescription"}}

{{.QrCode}}
- {{t "MfaInitVerify.Secret"}} + {{t "MFAInitVerify.Secret"}} {{.Secret}} content_copy
- +
@@ -37,7 +37,7 @@
- + {{t "Actions.Back"}} diff --git a/internal/ui/login/static/templates/mfa_prompt.html b/internal/ui/login/static/templates/mfa_prompt.html index cf9dac3b68..d5ae8efab0 100644 --- a/internal/ui/login/static/templates/mfa_prompt.html +++ b/internal/ui/login/static/templates/mfa_prompt.html @@ -3,7 +3,7 @@
{{ template "user-profile" . }} -

{{t "MfaPrompt.Description"}}

+

{{t "MFAPrompt.Description"}}

@@ -13,8 +13,8 @@
- {{ range $provider := .MfaProviders}} - {{ $providerName := (t (printf "MfaPrompt.Provider%v" $provider)) }} + {{ range $provider := .MFAProviders}} + {{ $providerName := (t (printf "MFAPrompt.Provider%v" $provider)) }}
@@ -24,7 +24,7 @@
- {{if not .MfaRequired}} + {{if not .MFARequired}} {{end}} diff --git a/internal/ui/login/static/templates/mfa_verification_u2f.html b/internal/ui/login/static/templates/mfa_verification_u2f.html new file mode 100644 index 0000000000..1e44ef72dc --- /dev/null +++ b/internal/ui/login/static/templates/mfa_verification_u2f.html @@ -0,0 +1,37 @@ +{{template "main-top" .}} + +
+ {{ template "user-profile" . }} + +

{{t "MFAVerifyU2F.Description"}}

+
+ + + + {{ .CSRF }} + + + + + +
+ + {{ template "error-message" .}} + +
+ +
+ + + + + + +{{template "main-bottom" .}} diff --git a/internal/ui/login/static/templates/mfa_verify.html b/internal/ui/login/static/templates/mfa_verify.html index eae77ba47e..8addc37495 100644 --- a/internal/ui/login/static/templates/mfa_verify.html +++ b/internal/ui/login/static/templates/mfa_verify.html @@ -3,7 +3,7 @@
{{ template "user-profile" . }} -

{{t "MfaVerify.Description"}}

+

{{t "MFAVerify.Description"}}

@@ -11,11 +11,11 @@ {{ .CSRF }} - +
- +
diff --git a/internal/ui/login/static/templates/passwordless.html b/internal/ui/login/static/templates/passwordless.html new file mode 100644 index 0000000000..a773234a8d --- /dev/null +++ b/internal/ui/login/static/templates/passwordless.html @@ -0,0 +1,37 @@ +{{template "main-top" .}} + +
+ {{ template "user-profile" . }} + +

{{t "Passwordless.Description"}}

+
+ + + + {{ .CSRF }} + + + + + +
+ + + +
+ + {{ template "error-message" .}} + +
+ +
+
+ + + + + +{{template "main-bottom" .}} diff --git a/internal/user/model/mfa.go b/internal/user/model/otp.go similarity index 55% rename from internal/user/model/mfa.go rename to internal/user/model/otp.go index 6a188c144e..3317ae6b99 100644 --- a/internal/user/model/mfa.go +++ b/internal/user/model/otp.go @@ -11,26 +11,27 @@ type OTP struct { Secret *crypto.CryptoValue SecretString string Url string - State MfaState + State MFAState } -type MfaState int32 +type MFAState int32 const ( - MfaStateUnspecified MfaState = iota - MfaStateNotReady - MfaStateReady + MFAStateUnspecified MFAState = iota + MFAStateNotReady + MFAStateReady ) type MultiFactor struct { - Type MfaType - State MfaState + Type MFAType + State MFAState + Attribute string } -type MfaType int32 +type MFAType int32 const ( - MfaTypeUnspecified MfaType = iota - MfaTypeOTP - MfaTypeSMS + MFATypeUnspecified MFAType = iota + MFATypeOTP + MFATypeU2F ) diff --git a/internal/user/model/user_human.go b/internal/user/model/user_human.go index 91a988b53c..c1af161fd5 100644 --- a/internal/user/model/user_human.go +++ b/internal/user/model/user_human.go @@ -1,9 +1,11 @@ package model import ( - iam_model "github.com/caos/zitadel/internal/iam/model" + "bytes" "time" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/crypto" es_models "github.com/caos/zitadel/internal/eventstore/models" ) @@ -14,12 +16,16 @@ type Human struct { *Email *Phone *Address - ExternalIDPs []*ExternalIDP - InitCode *InitUserCode - EmailCode *EmailCode - PhoneCode *PhoneCode - PasswordCode *PasswordCode - OTP *OTP + ExternalIDPs []*ExternalIDP + InitCode *InitUserCode + EmailCode *EmailCode + PhoneCode *PhoneCode + PasswordCode *PasswordCode + OTP *OTP + U2FTokens []*WebAuthNToken + PasswordlessTokens []*WebAuthNToken + U2FLogins []*WebAuthNLogin + PasswordlessLogins []*WebAuthNLogin } type InitUserCode struct { @@ -53,7 +59,7 @@ func (u *Human) IsInitialState() bool { } func (u *Human) IsOTPReady() bool { - return u.OTP != nil && u.OTP.State == MfaStateReady + return u.OTP != nil && u.OTP.State == MFAStateReady } func (u *Human) HashPasswordIfExisting(policy *iam_model.PasswordComplexityPolicyView, passwordAlg crypto.HashAlgorithm, onetime bool) error { @@ -105,3 +111,75 @@ func (u *Human) GetExternalIDP(externalIDP *ExternalIDP) (int, *ExternalIDP) { } return -1, nil } + +func (u *Human) GetU2F(webAuthNTokenID string) (int, *WebAuthNToken) { + for i, u2f := range u.U2FTokens { + if u2f.WebAuthNTokenID == webAuthNTokenID { + return i, u2f + } + } + return -1, nil +} + +func (u *Human) GetU2FByKeyID(keyID []byte) (int, *WebAuthNToken) { + for i, u2f := range u.U2FTokens { + if bytes.Compare(u2f.KeyID, keyID) == 0 { + return i, u2f + } + } + return -1, nil +} + +func (u *Human) GetU2FToVerify() (int, *WebAuthNToken) { + for i, u2f := range u.U2FTokens { + if u2f.State == MFAStateNotReady { + return i, u2f + } + } + return -1, nil +} + +func (u *Human) GetPasswordless(webAuthNTokenID string) (int, *WebAuthNToken) { + for i, u2f := range u.PasswordlessTokens { + if u2f.WebAuthNTokenID == webAuthNTokenID { + return i, u2f + } + } + return -1, nil +} + +func (u *Human) GetPasswordlessByKeyID(keyID []byte) (int, *WebAuthNToken) { + for i, pwl := range u.PasswordlessTokens { + if bytes.Compare(pwl.KeyID, keyID) == 0 { + return i, pwl + } + } + return -1, nil +} + +func (u *Human) GetPasswordlessToVerify() (int, *WebAuthNToken) { + for i, u2f := range u.PasswordlessTokens { + if u2f.State == MFAStateNotReady { + return i, u2f + } + } + return -1, nil +} + +func (u *Human) GetU2FLogin(authReqID string) (int, *WebAuthNLogin) { + for i, u2f := range u.U2FLogins { + if u2f.AuthRequest.ID == authReqID { + return i, u2f + } + } + return -1, nil +} + +func (u *Human) GetPasswordlessLogin(authReqID string) (int, *WebAuthNLogin) { + for i, pw := range u.PasswordlessLogins { + if pw.AuthRequest.ID == authReqID { + return i, pw + } + } + return -1, nil +} diff --git a/internal/user/model/user_session_view.go b/internal/user/model/user_session_view.go index fd1a43c51b..9860c7d9ed 100644 --- a/internal/user/model/user_session_view.go +++ b/internal/user/model/user_session_view.go @@ -19,6 +19,7 @@ type UserSessionView struct { DisplayName string SelectedIDPConfigID string PasswordVerification time.Time + PasswordlessVerification time.Time ExternalLoginVerification time.Time SecondFactorVerification time.Time SecondFactorVerificationType req_model.MFAType diff --git a/internal/user/model/user_view.go b/internal/user/model/user_view.go index 96826f0e4e..019c92bbe3 100644 --- a/internal/user/model/user_view.go +++ b/internal/user/model/user_view.go @@ -1,14 +1,15 @@ package model import ( - iam_model "github.com/caos/zitadel/internal/iam/model" "time" + "golang.org/x/text/language" + req_model "github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore/models" + iam_model "github.com/caos/zitadel/internal/iam/model" "github.com/caos/zitadel/internal/model" - "golang.org/x/text/language" ) type UserView struct { @@ -46,12 +47,20 @@ type HumanView struct { PostalCode string Region string StreetAddress string - OTPState MfaState - MfaMaxSetUp req_model.MFALevel - MfaInitSkipped time.Time + OTPState MFAState + U2FTokens []*WebAuthNView + PasswordlessTokens []*WebAuthNView + MFAMaxSetUp req_model.MFALevel + MFAInitSkipped time.Time InitRequired bool } +type WebAuthNView struct { + TokenID string + Name string + State MFAState +} + type MachineView struct { LastKeyAdded time.Time Name string @@ -108,7 +117,7 @@ func (r *UserSearchRequest) AppendMyOrgQuery(orgID string) { r.Queries = append(r.Queries, &UserSearchQuery{Key: UserSearchKeyResourceOwner, Method: model.SearchMethodEquals, Value: orgID}) } -func (u *UserView) MfaTypesSetupPossible(level req_model.MFALevel, policy *iam_model.LoginPolicyView) []req_model.MFAType { +func (u *UserView) MFATypesSetupPossible(level req_model.MFALevel, policy *iam_model.LoginPolicyView) []req_model.MFAType { types := make([]req_model.MFAType, 0) switch level { default: @@ -118,13 +127,14 @@ func (u *UserView) MfaTypesSetupPossible(level req_model.MFALevel, policy *iam_m for _, mfaType := range policy.SecondFactors { switch mfaType { case iam_model.SecondFactorTypeOTP: - if u.OTPState != MfaStateReady { + if u.OTPState != MFAStateReady { types = append(types, req_model.MFATypeOTP) } + case iam_model.SecondFactorTypeU2F: + types = append(types, req_model.MFATypeU2F) } } } - //PLANNED: add sms fallthrough case req_model.MFALevelMultiFactor: @@ -132,17 +142,15 @@ func (u *UserView) MfaTypesSetupPossible(level req_model.MFALevel, policy *iam_m for _, mfaType := range policy.MultiFactors { switch mfaType { case iam_model.MultiFactorTypeU2FWithPIN: - // TODO: Check if not set up already - // types = append(types, req_model.MFATypeU2F) + types = append(types, req_model.MFATypeU2FUserVerification) } } } - //PLANNED: add token } return types } -func (u *UserView) MfaTypesAllowed(level req_model.MFALevel, policy *iam_model.LoginPolicyView) ([]req_model.MFAType, bool) { +func (u *UserView) MFATypesAllowed(level req_model.MFALevel, policy *iam_model.LoginPolicyView) ([]req_model.MFAType, bool) { types := make([]req_model.MFAType, 0) required := true switch level { @@ -154,9 +162,13 @@ func (u *UserView) MfaTypesAllowed(level req_model.MFALevel, policy *iam_model.L for _, mfaType := range policy.SecondFactors { switch mfaType { case iam_model.SecondFactorTypeOTP: - if u.OTPState == MfaStateReady { + if u.OTPState == MFAStateReady { types = append(types, req_model.MFATypeOTP) } + case iam_model.SecondFactorTypeU2F: + if u.IsU2FReady() { + types = append(types, req_model.MFATypeU2F) + } } } } @@ -167,8 +179,9 @@ func (u *UserView) MfaTypesAllowed(level req_model.MFALevel, policy *iam_model.L for _, mfaType := range policy.MultiFactors { switch mfaType { case iam_model.MultiFactorTypeU2FWithPIN: - // TODO: Check if not set up already - // types = append(types, req_model.MFATypeU2F) + if u.IsPasswordlessReady() { + types = append(types, req_model.MFATypeU2FUserVerification) + } } } } @@ -177,15 +190,33 @@ func (u *UserView) MfaTypesAllowed(level req_model.MFALevel, policy *iam_model.L return types, required } +func (u *UserView) IsU2FReady() bool { + for _, token := range u.U2FTokens { + if token.State == MFAStateReady { + return true + } + } + return false +} + +func (u *UserView) IsPasswordlessReady() bool { + for _, token := range u.PasswordlessTokens { + if token.State == MFAStateReady { + return true + } + } + return false +} + func (u *UserView) HasRequiredOrgMFALevel(policy *iam_model.LoginPolicyView) bool { if !policy.ForceMFA { return true } - switch u.MfaMaxSetUp { + switch u.MFAMaxSetUp { case req_model.MFALevelSecondFactor: return policy.HasSecondFactors() case req_model.MFALevelMultiFactor: - return true + return policy.HasMultiFactors() default: return false } diff --git a/internal/user/model/web_auth_n.go b/internal/user/model/web_auth_n.go new file mode 100644 index 0000000000..9164480aae --- /dev/null +++ b/internal/user/model/web_auth_n.go @@ -0,0 +1,58 @@ +package model + +import ( + "github.com/caos/zitadel/internal/auth_request/model" + es_models "github.com/caos/zitadel/internal/eventstore/models" +) + +type WebAuthNToken struct { + es_models.ObjectRoot + + WebAuthNTokenID string + CredentialCreationData []byte + State MFAState + Challenge string + AllowedCredentialIDs [][]byte + UserVerification UserVerificationRequirement + KeyID []byte + PublicKey []byte + AttestationType string + AAGUID []byte + SignCount uint32 + WebAuthNTokenName string +} + +type WebAuthNLogin struct { + es_models.ObjectRoot + + CredentialAssertionData []byte + Challenge string + AllowedCredentialIDs [][]byte + UserVerification UserVerificationRequirement + *model.AuthRequest +} + +type WebAuthNMethod int32 + +const ( + WebAuthNMethodUnspecified WebAuthNMethod = iota + WebAuthNMethodU2F + WebAuthNMethodPasswordless +) + +type UserVerificationRequirement int32 + +const ( + UserVerificationRequirementUnspecified UserVerificationRequirement = iota + UserVerificationRequirementRequired + UserVerificationRequirementPreferred + UserVerificationRequirementDiscouraged +) + +type AuthenticatorAttachment int32 + +const ( + AuthenticatorAttachmentUnspecified AuthenticatorAttachment = iota + AuthenticatorAttachmentPlattform + AuthenticatorAttachmentCrossPlattform +) diff --git a/internal/user/repository/eventsourcing/eventstore.go b/internal/user/repository/eventsourcing/eventstore.go index 9fce4c30dc..20fb408a4d 100644 --- a/internal/user/repository/eventsourcing/eventstore.go +++ b/internal/user/repository/eventsourcing/eventstore.go @@ -24,6 +24,7 @@ import ( "github.com/caos/zitadel/internal/telemetry/tracing" usr_model "github.com/caos/zitadel/internal/user/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + webauthn_helper "github.com/caos/zitadel/internal/webauthn" ) const ( @@ -45,6 +46,7 @@ type UserEventstore struct { MachineKeySize int Multifactors global_model.Multifactors validateTOTP func(string, string) bool + webauthn *webauthn_helper.WebAuthN } type UserConfig struct { @@ -66,9 +68,12 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto emailVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.EmailVerificationCode, aesCrypto) phoneVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PhoneVerificationCode, aesCrypto) passwordVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PasswordVerificationCode, aesCrypto) - aesOtpCrypto, err := crypto.NewAESCrypto(systemDefaults.Multifactors.OTP.VerificationKey) + aesOTPCrypto, err := crypto.NewAESCrypto(systemDefaults.Multifactors.OTP.VerificationKey) passwordAlg := crypto.NewBCrypt(systemDefaults.SecretGenerators.PasswordSaltCost) - + web, err := webauthn_helper.StartServer(systemDefaults.WebAuthN.DisplayName, systemDefaults.WebAuthN.ID, systemDefaults.WebAuthN.Origin) + if err != nil { + return nil, err + } return &UserEventstore{ Eventstore: conf.Eventstore, userCache: userCache, @@ -80,7 +85,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto PasswordVerificationCode: passwordVerificationCode, Multifactors: global_model.Multifactors{ OTP: global_model.OTP{ - CryptoMFA: aesOtpCrypto, + CryptoMFA: aesOTPCrypto, Issuer: systemDefaults.Multifactors.OTP.Issuer, }, }, @@ -88,6 +93,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto validateTOTP: totp.Validate, MachineKeyAlg: aesCrypto, MachineKeySize: int(systemDefaults.SecretGenerators.MachineKeySize), + webauthn: web, }, nil } @@ -109,6 +115,20 @@ func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.U return model.UserToModel(user), nil } +func (es *UserEventstore) HumanByID(ctx context.Context, userID string) (*usr_model.User, error) { + if userID == "" { + return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-3M9sf", "Errors.User.UserIDMissing") + } + user, err := es.UserByID(ctx, userID) + if err != nil { + return nil, err + } + if user.Human == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-jLHYG", "Errors.User.NotHuman") + } + return user, nil +} + func (es *UserEventstore) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) { query, err := UserByIDQuery(id, sequence) if err != nil { @@ -404,17 +424,10 @@ func ChangesQuery(userID string, latestSequence, limit uint64, sortAscending boo } func (es *UserEventstore) InitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d8diw", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-mDPtj", "Errors.User.NotHuman") - } - if user.InitCode != nil { return user.InitCode, nil } @@ -422,16 +435,10 @@ func (es *UserEventstore) InitializeUserCodeByID(ctx context.Context, userID str } func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-9bbXj", "Errors.User.NotHuman") - } initCode := new(usr_model.InitUserCode) err = initCode.GenerateInitUserCode(es.InitializeUserCode) @@ -452,16 +459,10 @@ func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, user } func (es *UserEventstore) InitCodeSent(ctx context.Context, userID string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-0posw", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-SvPa6", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) agg := UserInitCodeSentAggregate(es.AggregateCreator(), repoUser) @@ -474,24 +475,18 @@ func (es *UserEventstore) InitCodeSent(ctx context.Context, userID string) error } func (es *UserEventstore) VerifyInitCode(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, verificationCode, password string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "Errors.User.UserIDMissing") + user, err := es.HumanByID(ctx, userID) + if err != nil { + return err } if verificationCode == "" { return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "Errors.User.Code.Empty") } pw := &usr_model.Password{SecretString: password} - err := pw.HashPasswordIfExisting(policy, es.PasswordAlg, false) + err = pw.HashPasswordIfExisting(policy, es.PasswordAlg, false) if err != nil { return err } - user, err := es.UserByID(ctx, userID) - if err != nil { - return err - } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-b3xda", "Errors.User.NotHuman") - } if user.InitCode == nil { return caos_errs.ThrowNotFound(nil, "EVENT-spo9W", "Errors.User.Code.NotFound") } @@ -514,20 +509,14 @@ func (es *UserEventstore) VerifyInitCode(ctx context.Context, policy *iam_model. return nil } -func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) +func (es *UserEventstore) SkipMFAInit(ctx context.Context, userID string) error { + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-S1tdl", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) - agg := SkipMfaAggregate(es.AggregateCreator(), repoUser) + agg := SkipMFAAggregate(es.AggregateCreator(), repoUser) err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg) if err != nil { return err @@ -537,16 +526,10 @@ func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error } func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) (*usr_model.Password, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-jLHYG", "Errors.User.NotHuman") - } if user.Password != nil { return user.Password, nil @@ -557,13 +540,10 @@ func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) ( func (es *UserEventstore) CheckPassword(ctx context.Context, userID, password string, authRequest *req_model.AuthRequest) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-HxcAx", "Errors.User.NotHuman") - } if user.Password == nil { return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s35Fa", "Errors.User.Password.Empty") } @@ -594,24 +574,18 @@ func (es *UserEventstore) setPasswordCheckResult(ctx context.Context, user *usr_ } func (es *UserEventstore) SetOneTimePassword(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, password *usr_model.Password) (*usr_model.Password, error) { - user, err := es.UserByID(ctx, password.AggregateID) + user, err := es.HumanByID(ctx, password.AggregateID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-PjDfJ", "Errors.User.NotHuman") - } return es.changedPassword(ctx, user, policy, password.SecretString, true) } func (es *UserEventstore) SetPassword(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, code, password string) error { - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-pHkAQ", "Errors.User.NotHuman") - } if user.PasswordCode == nil { return caos_errs.ThrowPreconditionFailed(nil, "EVENT-65sdr", "Errors.User.Code.NotFound") } @@ -623,13 +597,10 @@ func (es *UserEventstore) SetPassword(ctx context.Context, policy *iam_model.Pas } func (es *UserEventstore) ExternalLoginChecked(ctx context.Context, userID string, authRequest *req_model.AuthRequest) error { - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-Gns8i", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) repoAuthRequest := model.AuthRequestFromModel(authRequest) agg := ExternalLoginCheckSucceededAggregate(es.AggregateCreator(), repoUser, repoAuthRequest) @@ -687,13 +658,11 @@ func (es *UserEventstore) ChangeMachine(ctx context.Context, machine *usr_model. func (es *UserEventstore) ChangePassword(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, old, new string) (_ *usr_model.Password, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - user, err := es.UserByID(ctx, userID) + + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-9AuLE", "Errors.User.NotHuman") - } if user.Password == nil { return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Fds3s", "Errors.User.Password.Empty") } @@ -727,16 +696,10 @@ func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.U } func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string, notifyType usr_model.NotificationType) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-33ywz", "Errors.User.NotHuman") - } if user.State == usr_model.UserStateInitial { return errors.ThrowPreconditionFailed(nil, "EVENT-Hs11s", "Errors.User.NotInitialised") } @@ -787,16 +750,10 @@ func (es *UserEventstore) ResendInitialMail(ctx context.Context, userID, email s } func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s09ow", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-tbVAo", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) agg := PasswordCodeSentAggregate(es.AggregateCreator(), repoUser) @@ -812,13 +769,10 @@ func (es *UserEventstore) AddExternalIDP(ctx context.Context, externalIDP *usr_m if externalIDP == nil || !externalIDP.IsValid() { return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Ek9s", "Errors.User.ExternalIDP.Invalid") } - existingUser, err := es.UserByID(ctx, externalIDP.AggregateID) + existingUser, err := es.HumanByID(ctx, externalIDP.AggregateID) if err != nil { return nil, err } - if existingUser.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Cnk8s", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(existingUser) repoExternalIDP := model.ExternalIDPFromModel(externalIDP) aggregates, err := ExternalIDPAddedAggregate(ctx, es.Eventstore.AggregateCreator(), repoUser, repoExternalIDP) @@ -846,13 +800,10 @@ func (es *UserEventstore) BulkAddExternalIDPs(ctx context.Context, userID string return caos_errs.ThrowPreconditionFailed(nil, "EVENT-idue3", "Errors.User.ExternalIDP.Invalid") } } - existingUser, err := es.UserByID(ctx, userID) + existingUser, err := es.HumanByID(ctx, userID) if err != nil { return err } - if existingUser.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-Cnk8s", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(existingUser) repoExternalIDPs := model.ExternalIDPsFromModel(externalIDPs) aggregates, err := ExternalIDPAddedAggregate(ctx, es.Eventstore.AggregateCreator(), repoUser, repoExternalIDPs...) @@ -872,13 +823,10 @@ func (es *UserEventstore) PrepareRemoveExternalIDP(ctx context.Context, external if externalIDP == nil || !externalIDP.IsValid() { return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-Cm8sj", "Errors.User.ExternalIDP.Invalid") } - existingUser, err := es.UserByID(ctx, externalIDP.AggregateID) + existingUser, err := es.HumanByID(ctx, externalIDP.AggregateID) if err != nil { return nil, nil, err } - if existingUser.Human == nil { - return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-E8iod", "Errors.User.NotHuman") - } _, existingIDP := existingUser.GetExternalIDP(externalIDP) if existingIDP == nil { return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-3Dh7s", "Errors.User.ExternalIDP.NotOnUser") @@ -906,16 +854,10 @@ func (es *UserEventstore) RemoveExternalIDP(ctx context.Context, externalIDP *us } func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-BaE4M", "Errors.User.NotHuman") - } if user.Profile != nil { return user.Profile, nil @@ -928,13 +870,10 @@ func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model. if !profile.IsValid() { return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d82i3", "Errors.User.ProfileInvalid") } - user, err := es.UserByID(ctx, profile.AggregateID) + user, err := es.HumanByID(ctx, profile.AggregateID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Xhw8Y", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) repoProfile := model.ProfileFromModel(profile) @@ -950,16 +889,10 @@ func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model. } func (es *UserEventstore) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-zHtOg", "Errors.User.NotHuman") - } if user.Email != nil { return user.Email, nil @@ -971,13 +904,10 @@ func (es *UserEventstore) ChangeEmail(ctx context.Context, email *usr_model.Emai if !email.IsValid() { return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "Errors.User.EmailInvalid") } - user, err := es.UserByID(ctx, email.AggregateID) + user, err := es.HumanByID(ctx, email.AggregateID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-tgBdL", "Errors.User.NotHuman") - } if user.State == usr_model.UserStateInitial { return nil, errors.ThrowPreconditionFailed(nil, "EVENT-3H4q", "Errors.User.NotInitialised") } @@ -1005,18 +935,12 @@ func (es *UserEventstore) ChangeEmail(ctx context.Context, email *usr_model.Emai } func (es *UserEventstore) VerifyEmail(ctx context.Context, userID, verificationCode string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "Errors.User.UserIDMissing") - } - if verificationCode == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-skDws", "Errors.User.Code.Empty") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-YgXu6", "Errors.User.NotHuman") + if verificationCode == "" { + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-skDws", "Errors.User.Code.Empty") } if user.EmailCode == nil { return caos_errs.ThrowNotFound(nil, "EVENT-lso9w", "Errors.User.Code.NotFound") @@ -1043,16 +967,10 @@ func (es *UserEventstore) setEmailVerifyResult(ctx context.Context, user *usr_mo } func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userID string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-hqUZP", "Errors.User.NotHuman") - } if user.State == usr_model.UserStateInitial { return errors.ThrowPreconditionFailed(nil, "EVENT-E3fbw", "Errors.User.NotInitialised") } @@ -1082,16 +1000,10 @@ func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userI } func (es *UserEventstore) EmailVerificationCodeSent(ctx context.Context, userID string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-spo0w", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-BcFVd", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) agg := EmailCodeSentAggregate(es.AggregateCreator(), repoUser) @@ -1104,16 +1016,10 @@ func (es *UserEventstore) EmailVerificationCodeSent(ctx context.Context, userID } func (es *UserEventstore) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-LwQeA", "Errors.User.NotHuman") - } if user.Phone != nil { return user.Phone, nil @@ -1125,13 +1031,10 @@ func (es *UserEventstore) ChangePhone(ctx context.Context, phone *usr_model.Phon if !phone.IsValid() { return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9s4", "Errors.User.PhoneInvalid") } - user, err := es.UserByID(ctx, phone.AggregateID) + user, err := es.HumanByID(ctx, phone.AggregateID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-oREkn", "Errors.User.NotHuman") - } phoneCode, err := phone.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode) if err != nil { @@ -1156,13 +1059,10 @@ func (es *UserEventstore) VerifyPhone(ctx context.Context, userID, verificationC if userID == "" || verificationCode == "" { return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dsi8s", "Errors.User.UserIDMissing") } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-UspdK", "Errors.User.NotHuman") - } if user.PhoneCode == nil { return caos_errs.ThrowNotFound(nil, "EVENT-slp0s", "Errors.User.Code.NotFound") } @@ -1188,16 +1088,10 @@ func (es *UserEventstore) setPhoneVerifyResult(ctx context.Context, user *usr_mo } func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userID string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9sw", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-eEi05", "Errors.User.NotHuman") - } if user.Phone == nil { return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp9fs", "Errors.User.PhoneNotFound") } @@ -1224,16 +1118,10 @@ func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userI } func (es *UserEventstore) PhoneVerificationCodeSent(ctx context.Context, userID string) error { - if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0wa", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-5bhOP", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) agg := PhoneCodeSentAggregate(es.AggregateCreator(), repoUser) @@ -1246,13 +1134,10 @@ func (es *UserEventstore) PhoneVerificationCodeSent(ctx context.Context, userID } func (es *UserEventstore) RemovePhone(ctx context.Context, userID string) error { - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-Satfl", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) removeAggregate := PhoneRemovedAggregate(es.AggregateCreator(), repoUser) err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, removeAggregate) @@ -1265,16 +1150,10 @@ func (es *UserEventstore) RemovePhone(ctx context.Context, userID string) error } func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di8ws", "Errors.User.UserIDMissing") - } - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-pHrLu", "Errors.User.NotHuman") - } if user.Address != nil { return user.Address, nil @@ -1283,13 +1162,10 @@ func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_ } func (es *UserEventstore) ChangeAddress(ctx context.Context, address *usr_model.Address) (*usr_model.Address, error) { - user, err := es.UserByID(ctx, address.AggregateID) + user, err := es.HumanByID(ctx, address.AggregateID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-crpHD", "Errors.User.NotHuman") - } repoUser := model.UserFromModel(user) repoAddress := model.AddressFromModel(address) @@ -1304,15 +1180,12 @@ func (es *UserEventstore) ChangeAddress(ctx context.Context, address *usr_model. } func (es *UserEventstore) AddOTP(ctx context.Context, userID, accountName string) (*usr_model.OTP, error) { - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return nil, err } - if user.Human == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-XJvu3", "Errors.User.NotHuman") - } if user.IsOTPReady() { - return nil, caos_errs.ThrowAlreadyExists(nil, "EVENT-do9se", "Errors.User.Mfa.Otp.AlreadyReady") + return nil, caos_errs.ThrowAlreadyExists(nil, "EVENT-do9se", "Errors.User.MFA.OTP.AlreadyReady") } if accountName == "" { accountName = user.UserName @@ -1344,15 +1217,12 @@ func (es *UserEventstore) AddOTP(ctx context.Context, userID, accountName string } func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error { - user, err := es.UserByID(ctx, userID) + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-WsBv9", "Errors.User.NotHuman") - } if user.OTP == nil { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "Errors.User.Mfa.Otp.NotExisting") + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "Errors.User.MFA.OTP.NotExisting") } repoUser := model.UserFromModel(user) updateAggregate := MFAOTPRemoveAggregate(es.AggregateCreator(), repoUser) @@ -1365,21 +1235,18 @@ func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error { return nil } -func (es *UserEventstore) CheckMfaOTPSetup(ctx context.Context, userID, code string) error { - user, err := es.UserByID(ctx, userID) +func (es *UserEventstore) CheckMFAOTPSetup(ctx context.Context, userID, code string) error { + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-7zRQM", "Errors.User.NotHuman") - } if user.OTP == nil { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-yERHV", "Errors.Users.Mfa.Otp.NotExisting") + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-yERHV", "Errors.Users.MFA.OTP.NotExisting") } if user.IsOTPReady() { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-qx4ls", "Errors.Users.Mfa.Otp.AlreadyReady") + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady") } - if err := es.verifyMfaOTP(user.OTP, code); err != nil { + if err := es.verifyMFAOTP(user.OTP, code); err != nil { return err } repoUser := model.UserFromModel(user) @@ -1392,23 +1259,20 @@ func (es *UserEventstore) CheckMfaOTPSetup(ctx context.Context, userID, code str return nil } -func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string, authRequest *req_model.AuthRequest) error { - user, err := es.UserByID(ctx, userID) +func (es *UserEventstore) CheckMFAOTP(ctx context.Context, userID, code string, authRequest *req_model.AuthRequest) error { + user, err := es.HumanByID(ctx, userID) if err != nil { return err } - if user.Human == nil { - return errors.ThrowPreconditionFailed(nil, "EVENT-ckqn5", "Errors.User.NotHuman") - } if !user.IsOTPReady() { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sd5NJ", "Errors.User.Mfa.Otp.NotReady") + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sd5NJ", "Errors.User.MFA.OTP.NotReady") } repoUser := model.UserFromModel(user) repoAuthReq := model.AuthRequestFromModel(authRequest) var aggregate func(*es_models.AggregateCreator, *model.User, *model.AuthRequest) es_sdk.AggregateFunc var checkErr error - if checkErr = es.verifyMfaOTP(user.OTP, code); checkErr != nil { + if checkErr = es.verifyMFAOTP(user.OTP, code); checkErr != nil { aggregate = MFAOTPCheckFailedAggregate } else { aggregate = MFAOTPCheckSucceededAggregate @@ -1425,7 +1289,7 @@ func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string, return nil } -func (es *UserEventstore) verifyMfaOTP(otp *usr_model.OTP, code string) error { +func (es *UserEventstore) verifyMFAOTP(otp *usr_model.OTP, code string) error { decrypt, err := crypto.DecryptString(otp.Secret, es.Multifactors.OTP.CryptoMFA) if err != nil { return err @@ -1433,11 +1297,222 @@ func (es *UserEventstore) verifyMfaOTP(otp *usr_model.OTP, code string) error { valid := es.validateTOTP(code, decrypt) if !valid { - return caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.Mfa.Otp.InvalidCode") + return caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode") } return nil } +func (es *UserEventstore) AddU2F(ctx context.Context, userID string) (*usr_model.WebAuthNToken, error) { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return nil, err + } + webAuthN, err := es.webauthn.BeginRegistration(user, usr_model.AuthenticatorAttachmentUnspecified, usr_model.UserVerificationRequirementDiscouraged, user.U2FTokens...) + if err != nil { + return nil, err + } + tokenID, err := es.idGenerator.Next() + if err != nil { + return nil, err + } + webAuthN.WebAuthNTokenID = tokenID + repoUser := model.UserFromModel(user) + repoWebAuthN := model.WebAuthNFromModel(webAuthN) + + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAU2FAddAggregate(es.AggregateCreator(), repoUser, repoWebAuthN)) + if err != nil { + return nil, err + } + return webAuthN, nil +} + +func (es *UserEventstore) VerifyU2FSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return err + } + _, token := user.Human.GetU2FToVerify() + webAuthN, err := es.webauthn.FinishRegistration(user, token, tokenName, credentialData) + if err != nil { + return err + } + repoUser := model.UserFromModel(user) + repoWebAuthN := model.WebAuthNVerifyFromModel(webAuthN) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAU2FVerifyAggregate(es.AggregateCreator(), repoUser, repoWebAuthN)) + if err != nil { + return err + } + es.userCache.cacheUser(repoUser) + return nil +} + +func (es *UserEventstore) RemoveU2FToken(ctx context.Context, userID, webAuthNTokenID string) error { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return err + } + if _, token := user.Human.GetU2F(webAuthNTokenID); token == nil { + return errors.ThrowPreconditionFailed(nil, "EVENT-2M9ds", "Errors.User.NotHuman") + } + repoUser := model.UserFromModel(user) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAU2FRemoveAggregate(es.AggregateCreator(), repoUser, &model.WebAuthNTokenID{webAuthNTokenID})) + if err != nil { + return err + } + es.userCache.cacheUser(repoUser) + return nil +} + +func (es *UserEventstore) BeginU2FLogin(ctx context.Context, userID string, authRequest *req_model.AuthRequest) (*usr_model.WebAuthNLogin, error) { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return nil, err + } + if user.U2FTokens == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-5Mk8s", "Errors.User.MFA.U2F.NotExisting") + } + + webAuthNLogin, err := es.webauthn.BeginLogin(user, usr_model.UserVerificationRequirementDiscouraged, user.U2FTokens...) + if err != nil { + return nil, err + } + webAuthNLogin.AuthRequest = authRequest + repoUser := model.UserFromModel(user) + repoWebAuthNLogin := model.WebAuthNLoginFromModel(webAuthNLogin) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAU2FBeginLoginAggregate(es.AggregateCreator(), repoUser, repoWebAuthNLogin)) + if err != nil { + return nil, err + } + return webAuthNLogin, nil +} + +func (es *UserEventstore) VerifyMFAU2F(ctx context.Context, userID string, credentialData []byte, authRequest *req_model.AuthRequest) error { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return err + } + _, u2f := user.GetU2FLogin(authRequest.ID) + keyID, signCount, finishErr := es.webauthn.FinishLogin(user, u2f, credentialData, user.U2FTokens...) + if finishErr != nil && keyID == nil { + return finishErr + } + + _, token := user.GetU2FByKeyID(keyID) + repoUser := model.UserFromModel(user) + repoAuthRequest := model.AuthRequestFromModel(authRequest) + + signAgg := MFAU2FSignCountAggregate(es.AggregateCreator(), repoUser, &model.WebAuthNSignCount{WebauthNTokenID: token.WebAuthNTokenID, SignCount: signCount}, repoAuthRequest, finishErr == nil) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, signAgg) + if err != nil { + return err + } + return finishErr +} + +func (es *UserEventstore) AddPasswordless(ctx context.Context, userID string) (*usr_model.WebAuthNToken, error) { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return nil, err + } + webAuthN, err := es.webauthn.BeginRegistration(user, usr_model.AuthenticatorAttachmentUnspecified, usr_model.UserVerificationRequirementRequired, user.PasswordlessTokens...) + if err != nil { + return nil, err + } + tokenID, err := es.idGenerator.Next() + if err != nil { + return nil, err + } + webAuthN.WebAuthNTokenID = tokenID + repoUser := model.UserFromModel(user) + repoWebAuthN := model.WebAuthNFromModel(webAuthN) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAPasswordlessAddAggregate(es.AggregateCreator(), repoUser, repoWebAuthN)) + if err != nil { + return nil, err + } + return webAuthN, nil +} + +func (es *UserEventstore) VerifyPasswordlessSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return err + } + _, token := user.Human.GetPasswordlessToVerify() + webAuthN, err := es.webauthn.FinishRegistration(user, token, tokenName, credentialData) + if err != nil { + return err + } + repoUser := model.UserFromModel(user) + repoWebAuthN := model.WebAuthNVerifyFromModel(webAuthN) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAPasswordlessVerifyAggregate(es.AggregateCreator(), repoUser, repoWebAuthN)) + if err != nil { + return err + } + es.userCache.cacheUser(repoUser) + return nil +} + +func (es *UserEventstore) RemovePasswordlessToken(ctx context.Context, userID, webAuthNTokenID string) error { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return err + } + if _, token := user.Human.GetPasswordless(webAuthNTokenID); token == nil { + return errors.ThrowPreconditionFailed(nil, "EVENT-5M0sw", "Errors.User.NotHuman") + } + repoUser := model.UserFromModel(user) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAPasswordlessRemoveAggregate(es.AggregateCreator(), repoUser, &model.WebAuthNTokenID{webAuthNTokenID})) + if err != nil { + return err + } + es.userCache.cacheUser(repoUser) + return nil +} + +func (es *UserEventstore) BeginPasswordlessLogin(ctx context.Context, userID string, authRequest *req_model.AuthRequest) (*usr_model.WebAuthNLogin, error) { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return nil, err + } + if user.PasswordlessTokens == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-5M9sd", "Errors.User.MFA.Passwordless.NotExisting") + } + webAuthNLogin, err := es.webauthn.BeginLogin(user, usr_model.UserVerificationRequirementRequired, user.PasswordlessTokens...) + if err != nil { + return nil, err + } + webAuthNLogin.AuthRequest = authRequest + repoUser := model.UserFromModel(user) + repoWebAuthNLogin := model.WebAuthNLoginFromModel(webAuthNLogin) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAPasswordlessBeginLoginAggregate(es.AggregateCreator(), repoUser, repoWebAuthNLogin)) + if err != nil { + return nil, err + } + return webAuthNLogin, nil +} + +func (es *UserEventstore) VerifyPasswordless(ctx context.Context, userID string, credentialData []byte, authRequest *req_model.AuthRequest) error { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return err + } + _, passwordless := user.GetPasswordlessLogin(authRequest.ID) + keyID, signCount, finishErr := es.webauthn.FinishLogin(user, passwordless, credentialData, user.PasswordlessTokens...) + if finishErr != nil && keyID == nil { + return finishErr + } + _, token := user.GetPasswordlessByKeyID(keyID) + repoUser := model.UserFromModel(user) + repoAuthRequest := model.AuthRequestFromModel(authRequest) + + signAgg := MFAPasswordlessSignCountAggregate(es.AggregateCreator(), repoUser, &model.WebAuthNSignCount{WebauthNTokenID: token.WebAuthNTokenID, SignCount: signCount}, repoAuthRequest, finishErr == nil) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, signAgg) + if err != nil { + return err + } + return finishErr +} + func (es *UserEventstore) SignOut(ctx context.Context, agentID string, userIDs []string) error { users := make([]*model.User, len(userIDs)) for i, id := range userIDs { diff --git a/internal/user/repository/eventsourcing/eventstore_mock_test.go b/internal/user/repository/eventsourcing/eventstore_mock_test.go index cf222718df..2ea1f973f0 100644 --- a/internal/user/repository/eventsourcing/eventstore_mock_test.go +++ b/internal/user/repository/eventsourcing/eventstore_mock_test.go @@ -442,10 +442,10 @@ func GetMockManipulateUserWithOTP(ctrl *gomock.Controller, decrypt, verified boo }, } dataUser, _ := json.Marshal(user) - dataOtp, _ := json.Marshal(otp) + dataOTP, _ := json.Marshal(otp) events := []*es_models.Event{ {AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: dataUser}, - {AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.MFAOTPAdded, Data: dataOtp}, + {AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.MFAOTPAdded, Data: dataOTP}, } if verified { events = append(events, &es_models.Event{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.MFAOTPVerified}) diff --git a/internal/user/repository/eventsourcing/eventstore_test.go b/internal/user/repository/eventsourcing/eventstore_test.go index 8dbd61a2b0..6eb7b0cc3d 100644 --- a/internal/user/repository/eventsourcing/eventstore_test.go +++ b/internal/user/repository/eventsourcing/eventstore_test.go @@ -1207,7 +1207,7 @@ func TestInitCodeVerify(t *testing.T) { } } -func TestSkipMfaInit(t *testing.T) { +func TestSkipMFAInit(t *testing.T) { ctrl := gomock.NewController(t) type args struct { es *UserEventstore @@ -1256,7 +1256,7 @@ func TestSkipMfaInit(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.es.SkipMfaInit(tt.args.ctx, tt.args.user.AggregateID) + err := tt.args.es.SkipMFAInit(tt.args.ctx, tt.args.user.AggregateID) if tt.res.errFunc == nil && err != nil { t.Errorf("rshould not get err") @@ -3479,7 +3479,7 @@ func TestAddOTP(t *testing.T) { } } -func TestCheckMfaOTPSetup(t *testing.T) { +func TestCheckMFAOTPSetup(t *testing.T) { ctrl := gomock.NewController(t) type args struct { es *UserEventstore @@ -3578,7 +3578,7 @@ func TestCheckMfaOTPSetup(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.es.CheckMfaOTPSetup(tt.args.ctx, tt.args.userID, tt.args.code) + err := tt.args.es.CheckMFAOTPSetup(tt.args.ctx, tt.args.userID, tt.args.code) if tt.res.errFunc == nil && err != nil { t.Errorf("result should not get err") @@ -3590,7 +3590,7 @@ func TestCheckMfaOTPSetup(t *testing.T) { } } -func TestCheckMfaOTP(t *testing.T) { +func TestCheckMFAOTP(t *testing.T) { ctrl := gomock.NewController(t) type args struct { es *UserEventstore @@ -3708,7 +3708,7 @@ func TestCheckMfaOTP(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.es.CheckMfaOTP(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.authRequest) + err := tt.args.es.CheckMFAOTP(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.authRequest) if tt.res.errFunc == nil && err != nil { t.Errorf("result should not get err, got : %v", err) diff --git a/internal/user/repository/eventsourcing/model/auth_request.go b/internal/user/repository/eventsourcing/model/auth_request.go index e66d88257c..436199c89d 100644 --- a/internal/user/repository/eventsourcing/model/auth_request.go +++ b/internal/user/repository/eventsourcing/model/auth_request.go @@ -18,12 +18,27 @@ type AuthRequest struct { } func AuthRequestFromModel(request *model.AuthRequest) *AuthRequest { - return &AuthRequest{ + req := &AuthRequest{ ID: request.ID, UserAgentID: request.AgentID, - BrowserInfo: BrowserInfoFromModel(request.BrowserInfo), SelectedIDPConfigID: request.SelectedIDPConfigID, } + if request.BrowserInfo != nil { + req.BrowserInfo = BrowserInfoFromModel(request.BrowserInfo) + } + return req +} + +func AuthRequestToModel(request *AuthRequest) *model.AuthRequest { + req := &model.AuthRequest{ + ID: request.ID, + AgentID: request.UserAgentID, + SelectedIDPConfigID: request.SelectedIDPConfigID, + } + if request.BrowserInfo != nil { + req.BrowserInfo = BrowserInfoToModel(request.BrowserInfo) + } + return req } type BrowserInfo struct { @@ -40,6 +55,13 @@ func BrowserInfoFromModel(info *model.BrowserInfo) *BrowserInfo { } } +func BrowserInfoToModel(info *BrowserInfo) *model.BrowserInfo { + return &model.BrowserInfo{ + UserAgent: info.UserAgent, + AcceptLanguage: info.AcceptLanguage, + RemoteIP: info.RemoteIP, + } +} func (a *AuthRequest) SetData(event *es_models.Event) error { if err := json.Unmarshal(event.Data, a); err != nil { logging.Log("EVEN-T5df6").WithError(err).Error("could not unmarshal event data") diff --git a/internal/user/repository/eventsourcing/model/mfa.go b/internal/user/repository/eventsourcing/model/otp.go similarity index 90% rename from internal/user/repository/eventsourcing/model/mfa.go rename to internal/user/repository/eventsourcing/model/otp.go index f5b18137ee..4b37964ad9 100644 --- a/internal/user/repository/eventsourcing/model/mfa.go +++ b/internal/user/repository/eventsourcing/model/otp.go @@ -2,7 +2,6 @@ package model import ( "encoding/json" - "github.com/caos/logging" "github.com/caos/zitadel/internal/crypto" caos_errs "github.com/caos/zitadel/internal/errors" @@ -29,19 +28,19 @@ func OTPToModel(otp *OTP) *model.OTP { return &model.OTP{ ObjectRoot: otp.ObjectRoot, Secret: otp.Secret, - State: model.MfaState(otp.State), + State: model.MFAState(otp.State), } } func (u *Human) appendOTPAddedEvent(event *es_models.Event) error { u.OTP = &OTP{ - State: int32(model.MfaStateNotReady), + State: int32(model.MFAStateNotReady), } return u.OTP.setData(event) } func (u *Human) appendOTPVerifiedEvent() { - u.OTP.State = int32(model.MfaStateReady) + u.OTP.State = int32(model.MFAStateReady) } func (u *Human) appendOTPRemovedEvent() { diff --git a/internal/user/repository/eventsourcing/model/mfa_test.go b/internal/user/repository/eventsourcing/model/otp_test.go similarity index 90% rename from internal/user/repository/eventsourcing/model/mfa_test.go rename to internal/user/repository/eventsourcing/model/otp_test.go index 3a298036e7..6cbf817262 100644 --- a/internal/user/repository/eventsourcing/model/mfa_test.go +++ b/internal/user/repository/eventsourcing/model/otp_test.go @@ -9,7 +9,7 @@ import ( "github.com/caos/zitadel/internal/user/model" ) -func TestAppendMfaOTPAddedEvent(t *testing.T) { +func TestAppendMFAOTPAddedEvent(t *testing.T) { type args struct { user *Human otp *OTP @@ -27,7 +27,7 @@ func TestAppendMfaOTPAddedEvent(t *testing.T) { otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}, event: &es_models.Event{}, }, - result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MfaStateNotReady)}}, + result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFAStateNotReady)}}, }, } for _, tt := range tests { @@ -44,7 +44,7 @@ func TestAppendMfaOTPAddedEvent(t *testing.T) { } } -func TestAppendMfaOTPVerifyEvent(t *testing.T) { +func TestAppendMFAOTPVerifyEvent(t *testing.T) { type args struct { user *Human otp *OTP @@ -62,7 +62,7 @@ func TestAppendMfaOTPVerifyEvent(t *testing.T) { otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}, event: &es_models.Event{}, }, - result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MfaStateReady)}}, + result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFAStateReady)}}, }, } for _, tt := range tests { @@ -79,7 +79,7 @@ func TestAppendMfaOTPVerifyEvent(t *testing.T) { } } -func TestAppendMfaOTPRemoveEvent(t *testing.T) { +func TestAppendMFAOTPRemoveEvent(t *testing.T) { type args struct { user *Human otp *OTP diff --git a/internal/user/repository/eventsourcing/model/types.go b/internal/user/repository/eventsourcing/model/types.go index bc5efdf1a1..f392fd997c 100644 --- a/internal/user/repository/eventsourcing/model/types.go +++ b/internal/user/repository/eventsourcing/model/types.go @@ -118,6 +118,22 @@ const ( HumanMFAOTPCheckFailed models.EventType = "user.human.mfa.otp.check.failed" HumanMFAInitSkipped models.EventType = "user.human.mfa.init.skipped" + HumanMFAU2FTokenAdded models.EventType = "user.human.mfa.u2f.token.added" + HumanMFAU2FTokenVerified models.EventType = "user.human.mfa.u2f.token.verified" + HumanMFAU2FTokenSignCountChanged models.EventType = "user.human.mfa.u2f.token.signcount.changed" + HumanMFAU2FTokenRemoved models.EventType = "user.human.mfa.u2f.token.removed" + HumanMFAU2FTokenBeginLogin models.EventType = "user.human.mfa.u2f.token.begin.login" + HumanMFAU2FTokenCheckSucceeded models.EventType = "user.human.mfa.u2f.token.check.succeeded" + HumanMFAU2FTokenCheckFailed models.EventType = "user.human.mfa.u2f.token.check.failed" + + HumanPasswordlessTokenAdded models.EventType = "user.human.passwordless.token.added" + HumanPasswordlessTokenVerified models.EventType = "user.human.passwordless.token.verified" + HumanPasswordlessTokenChangeSignCount models.EventType = "user.human.passwordless.token.signcount.changed" + HumanPasswordlessTokenRemoved models.EventType = "user.human.passwordless.token.removed" + HumanPasswordlessTokenBeginLogin models.EventType = "user.human.passwordless.token.begin.login" + HumanPasswordlessTokenCheckSucceeded models.EventType = "user.human.passwordless.token.check.succeeded" + HumanPasswordlessTokenCheckFailed models.EventType = "user.human.passwordless.token.check.failed" + HumanSignedOut models.EventType = "user.human.signed.out" ) diff --git a/internal/user/repository/eventsourcing/model/user_human.go b/internal/user/repository/eventsourcing/model/user_human.go index c091cc5b26..36d6e33ac8 100644 --- a/internal/user/repository/eventsourcing/model/user_human.go +++ b/internal/user/repository/eventsourcing/model/user_human.go @@ -19,12 +19,16 @@ type Human struct { *Email *Phone *Address - ExternalIDPs []*ExternalIDP `json:"-"` - InitCode *InitUserCode `json:"-"` - EmailCode *EmailCode `json:"-"` - PhoneCode *PhoneCode `json:"-"` - PasswordCode *PasswordCode `json:"-"` - OTP *OTP `json:"-"` + ExternalIDPs []*ExternalIDP `json:"-"` + InitCode *InitUserCode `json:"-"` + EmailCode *EmailCode `json:"-"` + PhoneCode *PhoneCode `json:"-"` + PasswordCode *PasswordCode `json:"-"` + OTP *OTP `json:"-"` + U2FTokens []*WebAuthNToken `json:"-"` + PasswordlessTokens []*WebAuthNToken `json:"-"` + U2FLogins []*WebAuthNLogin `json:"-"` + PasswordlessLogins []*WebAuthNLogin `json:"-"` } type InitUserCode struct { @@ -56,6 +60,15 @@ func HumanFromModel(user *model.Human) *Human { if user.ExternalIDPs != nil { human.ExternalIDPs = ExternalIDPsFromModel(user.ExternalIDPs) } + if user.U2FTokens != nil { + human.U2FTokens = WebAuthNsFromModel(user.U2FTokens) + } + if user.PasswordlessTokens != nil { + human.PasswordlessTokens = WebAuthNsFromModel(user.PasswordlessTokens) + } + if user.U2FLogins != nil { + human.U2FLogins = WebAuthNLoginsFromModel(user.U2FLogins) + } return human } @@ -94,6 +107,15 @@ func HumanToModel(user *Human) *model.Human { if user.OTP != nil { human.OTP = OTPToModel(user.OTP) } + if user.U2FTokens != nil { + human.U2FTokens = WebAuthNsToModel(user.U2FTokens) + } + if user.PasswordlessTokens != nil { + human.PasswordlessTokens = WebAuthNsToModel(user.PasswordlessTokens) + } + if user.U2FLogins != nil { + human.U2FLogins = WebAuthNLoginsToModel(user.U2FLogins) + } return human } @@ -133,10 +155,10 @@ func (h *Human) AppendEvent(event *es_models.Event) (err error) { HumanAdded, HumanRegistered, HumanProfileChanged: - h.setData(event) + err = h.setData(event) case InitializedUserCodeAdded, InitializedHumanCodeAdded: - h.appendInitUsercodeCreatedEvent(event) + err = h.appendInitUsercodeCreatedEvent(event) case UserPasswordChanged, HumanPasswordChanged: err = h.appendUserPasswordChangedEvent(event) @@ -180,6 +202,26 @@ func (h *Human) AppendEvent(event *es_models.Event) (err error) { err = h.appendExternalIDPAddedEvent(event) case HumanExternalIDPRemoved, HumanExternalIDPCascadeRemoved: err = h.appendExternalIDPRemovedEvent(event) + case HumanMFAU2FTokenAdded: + err = h.appendU2FAddedEvent(event) + case HumanMFAU2FTokenVerified: + err = h.appendU2FVerifiedEvent(event) + case HumanMFAU2FTokenSignCountChanged: + err = h.appendU2FChangeSignCountEvent(event) + case HumanMFAU2FTokenRemoved: + err = h.appendU2FRemovedEvent(event) + case HumanPasswordlessTokenAdded: + err = h.appendPasswordlessAddedEvent(event) + case HumanPasswordlessTokenVerified: + err = h.appendPasswordlessVerifiedEvent(event) + case HumanPasswordlessTokenChangeSignCount: + err = h.appendPasswordlessChangeSignCountEvent(event) + case HumanPasswordlessTokenRemoved: + err = h.appendPasswordlessRemovedEvent(event) + case HumanMFAU2FTokenBeginLogin: + err = h.appendU2FLoginEvent(event) + case HumanPasswordlessTokenBeginLogin: + err = h.appendPasswordlessLoginEvent(event) } if err != nil { return err diff --git a/internal/user/repository/eventsourcing/model/web_auth_n.go b/internal/user/repository/eventsourcing/model/web_auth_n.go new file mode 100644 index 0000000000..f068a92ab0 --- /dev/null +++ b/internal/user/repository/eventsourcing/model/web_auth_n.go @@ -0,0 +1,336 @@ +package model + +import ( + "encoding/json" + + "github.com/caos/logging" + + caos_errs "github.com/caos/zitadel/internal/errors" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/user/model" +) + +type WebAuthNToken struct { + es_models.ObjectRoot + + WebauthNTokenID string `json:"webAuthNTokenId"` + Challenge string `json:"challenge"` + State int32 `json:"-"` + + KeyID []byte `json:"keyId"` + PublicKey []byte `json:"publicKey"` + AttestationType string `json:"attestationType"` + AAGUID []byte `json:"aaguid"` + SignCount uint32 `json:"signCount"` +} + +type WebAuthNVerify struct { + WebAuthNTokenID string `json:"webAuthNTokenId"` + KeyID []byte `json:"keyId"` + PublicKey []byte `json:"publicKey"` + AttestationType string `json:"attestationType"` + AAGUID []byte `json:"aaguid"` + SignCount uint32 `json:"signCount"` + WebAuthNTokenName string `json:"webAuthNTokenName"` +} + +type WebAuthNSignCount struct { + WebauthNTokenID string `json:"webAuthNTokenId"` + SignCount uint32 `json:"signCount"` +} + +type WebAuthNTokenID struct { + WebauthNTokenID string `json:"webAuthNTokenId"` +} + +type WebAuthNLogin struct { + es_models.ObjectRoot + + WebauthNTokenID string `json:"webAuthNTokenId"` + Challenge string `json:"challenge"` + *AuthRequest +} + +func GetWebauthn(webauthnTokens []*WebAuthNToken, id string) (int, *WebAuthNToken) { + for i, webauthn := range webauthnTokens { + if webauthn.WebauthNTokenID == id { + return i, webauthn + } + } + return -1, nil +} + +func WebAuthNsToModel(u2fs []*WebAuthNToken) []*model.WebAuthNToken { + convertedIDPs := make([]*model.WebAuthNToken, len(u2fs)) + for i, m := range u2fs { + convertedIDPs[i] = WebAuthNToModel(m) + } + return convertedIDPs +} + +func WebAuthNsFromModel(u2fs []*model.WebAuthNToken) []*WebAuthNToken { + convertedIDPs := make([]*WebAuthNToken, len(u2fs)) + for i, m := range u2fs { + convertedIDPs[i] = WebAuthNFromModel(m) + } + return convertedIDPs +} + +func WebAuthNFromModel(webAuthN *model.WebAuthNToken) *WebAuthNToken { + return &WebAuthNToken{ + ObjectRoot: webAuthN.ObjectRoot, + WebauthNTokenID: webAuthN.WebAuthNTokenID, + Challenge: webAuthN.Challenge, + State: int32(webAuthN.State), + KeyID: webAuthN.KeyID, + PublicKey: webAuthN.PublicKey, + AAGUID: webAuthN.AAGUID, + SignCount: webAuthN.SignCount, + AttestationType: webAuthN.AttestationType, + } +} + +func WebAuthNToModel(webAuthN *WebAuthNToken) *model.WebAuthNToken { + return &model.WebAuthNToken{ + ObjectRoot: webAuthN.ObjectRoot, + WebAuthNTokenID: webAuthN.WebauthNTokenID, + Challenge: webAuthN.Challenge, + State: model.MFAState(webAuthN.State), + KeyID: webAuthN.KeyID, + PublicKey: webAuthN.PublicKey, + AAGUID: webAuthN.AAGUID, + SignCount: webAuthN.SignCount, + AttestationType: webAuthN.AttestationType, + } +} + +func WebAuthNVerifyFromModel(webAuthN *model.WebAuthNToken) *WebAuthNVerify { + return &WebAuthNVerify{ + WebAuthNTokenID: webAuthN.WebAuthNTokenID, + KeyID: webAuthN.KeyID, + PublicKey: webAuthN.PublicKey, + AAGUID: webAuthN.AAGUID, + SignCount: webAuthN.SignCount, + AttestationType: webAuthN.AttestationType, + WebAuthNTokenName: webAuthN.WebAuthNTokenName, + } +} + +func WebAuthNLoginsToModel(u2fs []*WebAuthNLogin) []*model.WebAuthNLogin { + convertedIDPs := make([]*model.WebAuthNLogin, len(u2fs)) + for i, m := range u2fs { + convertedIDPs[i] = WebAuthNLoginToModel(m) + } + return convertedIDPs +} + +func WebAuthNLoginsFromModel(u2fs []*model.WebAuthNLogin) []*WebAuthNLogin { + convertedIDPs := make([]*WebAuthNLogin, len(u2fs)) + for i, m := range u2fs { + convertedIDPs[i] = WebAuthNLoginFromModel(m) + } + return convertedIDPs +} + +func WebAuthNLoginFromModel(webAuthN *model.WebAuthNLogin) *WebAuthNLogin { + return &WebAuthNLogin{ + ObjectRoot: webAuthN.ObjectRoot, + Challenge: webAuthN.Challenge, + AuthRequest: AuthRequestFromModel(webAuthN.AuthRequest), + } +} + +func WebAuthNLoginToModel(webAuthN *WebAuthNLogin) *model.WebAuthNLogin { + return &model.WebAuthNLogin{ + ObjectRoot: webAuthN.ObjectRoot, + Challenge: webAuthN.Challenge, + AuthRequest: AuthRequestToModel(webAuthN.AuthRequest), + } +} + +func (u *Human) appendU2FAddedEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + webauthn.ObjectRoot.CreationDate = event.CreationDate + webauthn.State = int32(model.MFAStateNotReady) + for i, token := range u.U2FTokens { + if token.State == int32(model.MFAStateNotReady) { + u.U2FTokens[i] = webauthn + return nil + } + } + u.U2FTokens = append(u.U2FTokens, webauthn) + return nil +} + +func (u *Human) appendU2FVerifiedEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + if _, token := GetWebauthn(u.U2FTokens, webauthn.WebauthNTokenID); token != nil { + err := token.setData(event) + if err != nil { + return err + } + token.State = int32(model.MFAStateReady) + return nil + } + return caos_errs.ThrowPreconditionFailed(nil, "MODEL-4hu9s", "Errors.Users.MFA.U2F.NotExisting") +} + +func (u *Human) appendU2FChangeSignCountEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + if _, token := GetWebauthn(u.U2FTokens, webauthn.WebauthNTokenID); token != nil { + token.setData(event) + return nil + } + return caos_errs.ThrowPreconditionFailed(nil, "MODEL-5Ms8h", "Errors.Users.MFA.U2F.NotExisting") +} + +func (u *Human) appendU2FRemovedEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + for i := len(u.U2FTokens) - 1; i >= 0; i-- { + if u.U2FTokens[i].WebauthNTokenID == webauthn.WebauthNTokenID { + copy(u.U2FTokens[i:], u.U2FTokens[i+1:]) + u.U2FTokens[len(u.U2FTokens)-1] = nil + u.U2FTokens = u.U2FTokens[:len(u.U2FTokens)-1] + return nil + } + } + return nil +} + +func (u *Human) appendPasswordlessAddedEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + webauthn.ObjectRoot.CreationDate = event.CreationDate + webauthn.State = int32(model.MFAStateNotReady) + for i, token := range u.PasswordlessTokens { + if token.State == int32(model.MFAStateNotReady) { + u.PasswordlessTokens[i] = webauthn + return nil + } + } + u.PasswordlessTokens = append(u.PasswordlessTokens, webauthn) + return nil +} + +func (u *Human) appendPasswordlessVerifiedEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + if _, token := GetWebauthn(u.PasswordlessTokens, webauthn.WebauthNTokenID); token != nil { + err := token.setData(event) + if err != nil { + return err + } + token.State = int32(model.MFAStateReady) + return nil + } + return caos_errs.ThrowPreconditionFailed(nil, "MODEL-mKns8", "Errors.Users.MFA.Passwordless.NotExisting") +} + +func (u *Human) appendPasswordlessChangeSignCountEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + if _, token := GetWebauthn(u.PasswordlessTokens, webauthn.WebauthNTokenID); token != nil { + err := token.setData(event) + if err != nil { + return err + } + return nil + } + return caos_errs.ThrowPreconditionFailed(nil, "MODEL-2Mv9s", "Errors.Users.MFA.Passwordless.NotExisting") +} + +func (u *Human) appendPasswordlessRemovedEvent(event *es_models.Event) error { + webauthn := new(WebAuthNToken) + err := webauthn.setData(event) + if err != nil { + return err + } + for i := len(u.PasswordlessTokens) - 1; i >= 0; i-- { + if u.PasswordlessTokens[i].WebauthNTokenID == webauthn.WebauthNTokenID { + copy(u.PasswordlessTokens[i:], u.PasswordlessTokens[i+1:]) + u.PasswordlessTokens[len(u.PasswordlessTokens)-1] = nil + u.PasswordlessTokens = u.PasswordlessTokens[:len(u.PasswordlessTokens)-1] + return nil + } + } + return nil +} + +func (w *WebAuthNToken) setData(event *es_models.Event) error { + w.ObjectRoot.AppendEvent(event) + if err := json.Unmarshal(event.Data, w); err != nil { + logging.Log("EVEN-4M9is").WithError(err).Error("could not unmarshal event data") + return caos_errs.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event") + } + return nil +} + +func (u *Human) appendU2FLoginEvent(event *es_models.Event) error { + webauthn := new(WebAuthNLogin) + webauthn.ObjectRoot.AppendEvent(event) + err := webauthn.setData(event) + if err != nil { + return err + } + webauthn.ObjectRoot.CreationDate = event.CreationDate + for i, token := range u.U2FLogins { + if token.AuthRequest.ID == webauthn.AuthRequest.ID { + u.U2FLogins[i] = webauthn + return nil + } + } + u.U2FLogins = append(u.U2FLogins, webauthn) + return nil +} + +func (u *Human) appendPasswordlessLoginEvent(event *es_models.Event) error { + webauthn := new(WebAuthNLogin) + webauthn.ObjectRoot.AppendEvent(event) + err := webauthn.setData(event) + if err != nil { + return err + } + webauthn.ObjectRoot.CreationDate = event.CreationDate + for i, token := range u.PasswordlessLogins { + if token.AuthRequest.ID == webauthn.AuthRequest.ID { + u.PasswordlessLogins[i] = webauthn + return nil + } + } + u.PasswordlessLogins = append(u.PasswordlessLogins, webauthn) + return nil +} + +func (w *WebAuthNLogin) setData(event *es_models.Event) error { + w.ObjectRoot.AppendEvent(event) + if err := json.Unmarshal(event.Data, w); err != nil { + logging.Log("EVEN-hmSlo").WithError(err).Error("could not unmarshal event data") + return caos_errs.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event") + } + return nil +} diff --git a/internal/user/repository/eventsourcing/model/web_auth_n_test.go b/internal/user/repository/eventsourcing/model/web_auth_n_test.go new file mode 100644 index 0000000000..b4b82b1bab --- /dev/null +++ b/internal/user/repository/eventsourcing/model/web_auth_n_test.go @@ -0,0 +1,151 @@ +package model + +import ( + "encoding/json" + "github.com/caos/zitadel/pkg/grpc/auth" + "testing" + + es_models "github.com/caos/zitadel/internal/eventstore/models" +) + +func TestAppendMFAU2FAddedEvent(t *testing.T) { + type args struct { + user *Human + u2f *WebAuthNToken + event *es_models.Event + } + tests := []struct { + name string + args args + result *Human + }{ + { + name: "append user u2f event", + args: args{ + user: &Human{}, + u2f: &WebAuthNToken{WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge"}, + event: &es_models.Event{}, + }, + result: &Human{ + U2FTokens: []*WebAuthNToken{ + {WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge", State: int32(auth.MFAState_MFASTATE_NOT_READY)}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.u2f != nil { + data, _ := json.Marshal(tt.args.u2f) + tt.args.event.Data = data + } + tt.args.user.appendU2FAddedEvent(tt.args.event) + if tt.args.user.U2FTokens[0].State != tt.result.U2FTokens[0].State { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].State, tt.args.user.U2FTokens[0].State) + } + }) + } +} + +func TestAppendMFAU2FVerifyEvent(t *testing.T) { + type args struct { + user *Human + u2f *WebAuthNVerify + event *es_models.Event + } + tests := []struct { + name string + args args + result *Human + }{ + { + name: "append u2f verify event", + args: args{ + user: &Human{ + U2FTokens: []*WebAuthNToken{ + {WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge", State: int32(auth.MFAState_MFASTATE_NOT_READY)}, + }, + }, + u2f: &WebAuthNVerify{WebAuthNTokenID: "WebauthNTokenID", KeyID: []byte("KeyID"), PublicKey: []byte("PublicKey"), AttestationType: "AttestationType", AAGUID: []byte("AAGUID"), SignCount: 1}, + event: &es_models.Event{}, + }, + result: &Human{ + U2FTokens: []*WebAuthNToken{ + { + WebauthNTokenID: "WebauthNTokenID", + Challenge: "Challenge", + State: int32(auth.MFAState_MFASTATE_READY), + KeyID: []byte("KeyID"), + PublicKey: []byte("PublicKey"), + AttestationType: "AttestationType", + AAGUID: []byte("AAGUID"), + SignCount: 1, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.u2f != nil { + data, _ := json.Marshal(tt.args.u2f) + tt.args.event.Data = data + } + tt.args.user.appendU2FVerifiedEvent(tt.args.event) + if tt.args.user.U2FTokens[0].State != tt.result.U2FTokens[0].State { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].State, tt.args.user.U2FTokens[0].State) + } + if tt.args.user.U2FTokens[0].AttestationType != tt.result.U2FTokens[0].AttestationType { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].AttestationType, tt.args.user.U2FTokens[0].AttestationType) + } + }) + } +} + +func TestAppendMFAU2FRemoveEvent(t *testing.T) { + type args struct { + user *Human + u2f *WebAuthNTokenID + event *es_models.Event + } + tests := []struct { + name string + args args + result *Human + }{ + { + name: "append u2f remove event", + args: args{ + user: &Human{ + U2FTokens: []*WebAuthNToken{ + { + WebauthNTokenID: "WebauthNTokenID", + Challenge: "Challenge", + State: int32(auth.MFAState_MFASTATE_NOT_READY), + KeyID: []byte("KeyID"), + PublicKey: []byte("PublicKey"), + AttestationType: "AttestationType", + AAGUID: []byte("AAGUID"), + SignCount: 1, + }, + }, + }, + u2f: &WebAuthNTokenID{WebauthNTokenID: "WebauthNTokenID"}, + event: &es_models.Event{}, + }, + result: &Human{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.u2f != nil { + data, _ := json.Marshal(tt.args.u2f) + tt.args.event.Data = data + } + tt.args.user.appendU2FRemovedEvent(tt.args.event) + if len(tt.args.user.U2FTokens) != 0 { + t.Errorf("got wrong result: actual: %v ", tt.result.U2FTokens) + } + }) + } +} diff --git a/internal/user/repository/eventsourcing/user.go b/internal/user/repository/eventsourcing/user.go index 2c5f568dfc..b4ed0bdf78 100644 --- a/internal/user/repository/eventsourcing/user.go +++ b/internal/user/repository/eventsourcing/user.go @@ -400,7 +400,7 @@ func InitCodeCheckFailedAggregate(aggCreator *es_models.AggregateCreator, user * } } -func SkipMfaAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) { +func SkipMFAAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) { return func(ctx context.Context) (*es_models.Aggregate, error) { agg, err := UserAggregate(ctx, aggCreator, user) if err != nil { @@ -773,7 +773,7 @@ func MFAOTPCheckFailedAggregate(aggCreator *es_models.AggregateCreator, user *mo } } -func MFAOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) { +func MFAOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, user *model.User) es_sdk.AggregateFunc { return func(ctx context.Context) (*es_models.Aggregate, error) { agg, err := UserAggregate(ctx, aggCreator, user) if err != nil { @@ -783,6 +783,132 @@ func MFAOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, user *model.U } } +func MFAU2FAddAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNToken) es_sdk.AggregateFunc { + return MFAWebauthNAddAggregate(aggCreator, user, webauthN, model.HumanMFAU2FTokenAdded) +} + +func MFAPasswordlessAddAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNToken) es_sdk.AggregateFunc { + return MFAWebauthNAddAggregate(aggCreator, user, webauthN, model.HumanPasswordlessTokenAdded) +} + +func MFAWebauthNAddAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNToken, event es_models.EventType) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if webauthN == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-4N90s", "Errors.Internal") + } + agg, err := UserAggregate(ctx, aggCreator, user) + if err != nil { + return nil, err + } + return agg.AppendEvent(event, webauthN) + } +} + +func MFAU2FVerifyAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNVerify) es_sdk.AggregateFunc { + return MFAWebauthNVerifyAggregate(aggCreator, user, webauthN, model.HumanMFAU2FTokenVerified) +} + +func MFAPasswordlessVerifyAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNVerify) es_sdk.AggregateFunc { + return MFAWebauthNVerifyAggregate(aggCreator, user, webauthN, model.HumanPasswordlessTokenVerified) +} + +func MFAWebauthNVerifyAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNVerify, event es_models.EventType) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if webauthN == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-4N90s", "Errors.Internal") + } + agg, err := UserAggregate(ctx, aggCreator, user) + if err != nil { + return nil, err + } + return agg.AppendEvent(event, webauthN) + } +} + +func MFAU2FSignCountAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNSignCount, check *model.AuthRequest, checkSucceeded bool) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if webauthN == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-4N90s", "Errors.Internal") + } + agg, err := UserAggregate(ctx, aggCreator, user) + if err != nil { + return nil, err + } + agg, err = agg.AppendEvent(model.HumanMFAU2FTokenSignCountChanged, webauthN) + if err != nil { + return nil, err + } + if checkSucceeded { + return agg.AppendEvent(model.HumanMFAU2FTokenCheckSucceeded, check) + } else { + return agg.AppendEvent(model.HumanMFAU2FTokenCheckFailed, check) + } + } +} + +func MFAPasswordlessSignCountAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNSignCount, check *model.AuthRequest, checkSucceeded bool) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if webauthN == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-4N90s", "Errors.Internal") + } + agg, err := UserAggregate(ctx, aggCreator, user) + if err != nil { + return nil, err + } + agg, err = agg.AppendEvent(model.HumanPasswordlessTokenChangeSignCount, webauthN) + if err != nil { + return nil, err + } + if checkSucceeded { + return agg.AppendEvent(model.HumanPasswordlessTokenCheckSucceeded, check) + } else { + return agg.AppendEvent(model.HumanPasswordlessTokenCheckFailed, check) + } + } +} + +func MFAU2FRemoveAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNTokenID) es_sdk.AggregateFunc { + return MFAWebauthNRemoveAggregate(aggCreator, user, webauthN, model.HumanMFAU2FTokenRemoved) +} + +func MFAPasswordlessRemoveAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNTokenID) es_sdk.AggregateFunc { + return MFAWebauthNRemoveAggregate(aggCreator, user, webauthN, model.HumanPasswordlessTokenRemoved) +} + +func MFAWebauthNRemoveAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNTokenID, event es_models.EventType) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if webauthN == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-4Ms9l", "Errors.Internal") + } + agg, err := UserAggregate(ctx, aggCreator, user) + if err != nil { + return nil, err + } + return agg.AppendEvent(event, webauthN) + } +} + +func MFAU2FBeginLoginAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNLogin) es_sdk.AggregateFunc { + return MFAWebauthNBeginLoginAggregate(aggCreator, user, webauthN, model.HumanMFAU2FTokenBeginLogin) +} + +func MFAPasswordlessBeginLoginAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNLogin) es_sdk.AggregateFunc { + return MFAWebauthNBeginLoginAggregate(aggCreator, user, webauthN, model.HumanPasswordlessTokenBeginLogin) +} + +func MFAWebauthNBeginLoginAggregate(aggCreator *es_models.AggregateCreator, user *model.User, webauthN *model.WebAuthNLogin, event es_models.EventType) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if webauthN == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-4N90s", "Errors.Internal") + } + agg, err := UserAggregate(ctx, aggCreator, user) + if err != nil { + return nil, err + } + return agg.AppendEvent(event, webauthN) + } +} + func SignOutAggregates(aggCreator *es_models.AggregateCreator, users []*model.User, agentID string) func(ctx context.Context) ([]*es_models.Aggregate, error) { return func(ctx context.Context) ([]*es_models.Aggregate, error) { aggregates := make([]*es_models.Aggregate, len(users)) diff --git a/internal/user/repository/eventsourcing/user_test.go b/internal/user/repository/eventsourcing/user_test.go index bd7000a55c..328828598e 100644 --- a/internal/user/repository/eventsourcing/user_test.go +++ b/internal/user/repository/eventsourcing/user_test.go @@ -991,7 +991,7 @@ func TestInitCodeCheckFailedAggregate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - agg, err := SkipMfaAggregate(tt.args.aggCreator, tt.args.user)(tt.args.ctx) + agg, err := SkipMFAAggregate(tt.args.aggCreator, tt.args.user)(tt.args.ctx) if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen { t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events)) @@ -1006,7 +1006,7 @@ func TestInitCodeCheckFailedAggregate(t *testing.T) { } } -func TestSkipMfaAggregate(t *testing.T) { +func TestSkipMFAAggregate(t *testing.T) { type args struct { ctx context.Context user *model.User diff --git a/internal/user/repository/view/model/user.go b/internal/user/repository/view/model/user.go index f0406eddab..b13031bc5e 100644 --- a/internal/user/repository/view/model/user.go +++ b/internal/user/repository/view/model/user.go @@ -1,18 +1,18 @@ package model import ( + "database/sql/driver" "encoding/json" - iam_model "github.com/caos/zitadel/internal/iam/model" "time" - org_model "github.com/caos/zitadel/internal/org/model" - "github.com/lib/pq" - "github.com/caos/logging" + "github.com/lib/pq" req_model "github.com/caos/zitadel/internal/auth_request/model" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore/models" + iam_model "github.com/caos/zitadel/internal/iam/model" + org_model "github.com/caos/zitadel/internal/org/model" "github.com/caos/zitadel/internal/user/model" es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" ) @@ -67,30 +67,57 @@ const ( ) type HumanView struct { - FirstName string `json:"firstName" gorm:"column:first_name"` - LastName string `json:"lastName" gorm:"column:last_name"` - NickName string `json:"nickName" gorm:"column:nick_name"` - DisplayName string `json:"displayName" gorm:"column:display_name"` - PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"` - Gender int32 `json:"gender" gorm:"column:gender"` - Email string `json:"email" gorm:"column:email"` - IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"` - Phone string `json:"phone" gorm:"column:phone"` - IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"` - Country string `json:"country" gorm:"column:country"` - Locality string `json:"locality" gorm:"column:locality"` - PostalCode string `json:"postalCode" gorm:"column:postal_code"` - Region string `json:"region" gorm:"column:region"` - StreetAddress string `json:"streetAddress" gorm:"column:street_address"` - OTPState int32 `json:"-" gorm:"column:otp_state"` - MfaMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"` - MfaInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"` - InitRequired bool `json:"-" gorm:"column:init_required"` + FirstName string `json:"firstName" gorm:"column:first_name"` + LastName string `json:"lastName" gorm:"column:last_name"` + NickName string `json:"nickName" gorm:"column:nick_name"` + DisplayName string `json:"displayName" gorm:"column:display_name"` + PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"` + Gender int32 `json:"gender" gorm:"column:gender"` + Email string `json:"email" gorm:"column:email"` + IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"` + Phone string `json:"phone" gorm:"column:phone"` + IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"` + Country string `json:"country" gorm:"column:country"` + Locality string `json:"locality" gorm:"column:locality"` + PostalCode string `json:"postalCode" gorm:"column:postal_code"` + Region string `json:"region" gorm:"column:region"` + StreetAddress string `json:"streetAddress" gorm:"column:street_address"` + OTPState int32 `json:"-" gorm:"column:otp_state"` + U2FTokens WebAuthNTokens `json:"-" gorm:"column:u2f_tokens"` + MFAMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"` + MFAInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"` + InitRequired bool `json:"-" gorm:"column:init_required"` - PasswordSet bool `json:"-" gorm:"column:password_set"` - PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"` - UsernameChangeRequired bool `json:"-" gorm:"column:username_change_required"` - PasswordChanged time.Time `json:"-" gorm:"column:password_change"` + PasswordSet bool `json:"-" gorm:"column:password_set"` + PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"` + UsernameChangeRequired bool `json:"-" gorm:"column:username_change_required"` + PasswordChanged time.Time `json:"-" gorm:"column:password_change"` + PasswordlessTokens WebAuthNTokens `json:"-" gorm:"column:passwordless_tokens"` +} + +type WebAuthNTokens []*WebAuthNView + +type WebAuthNView struct { + ID string `json:"webAuthNTokenId"` + Name string `json:"webAuthNTokenName,omitempty"` + State int32 `json:"state,omitempty"` +} + +func (t WebAuthNTokens) Value() (driver.Value, error) { + if t == nil { + return nil, nil + } + return json.Marshal(&t) +} + +func (t *WebAuthNTokens) Scan(src interface{}) error { + if b, ok := src.([]byte); ok { + return json.Unmarshal(b, t) + } + if s, ok := src.(string); ok { + return json.Unmarshal([]byte(s), t) + } + return nil } func (h *HumanView) IsZero() bool { @@ -124,6 +151,8 @@ func UserToModel(user *UserView) *model.UserView { PasswordSet: user.PasswordSet, PasswordChangeRequired: user.PasswordChangeRequired, PasswordChanged: user.PasswordChanged, + PasswordlessTokens: WebauthnTokensToModel(user.PasswordlessTokens), + U2FTokens: WebauthnTokensToModel(user.U2FTokens), FirstName: user.FirstName, LastName: user.LastName, NickName: user.NickName, @@ -139,9 +168,9 @@ func UserToModel(user *UserView) *model.UserView { PostalCode: user.PostalCode, Region: user.Region, StreetAddress: user.StreetAddress, - OTPState: model.MfaState(user.OTPState), - MfaMaxSetUp: req_model.MFALevel(user.MfaMaxSetUp), - MfaInitSkipped: user.MfaInitSkipped, + OTPState: model.MFAState(user.OTPState), + MFAMaxSetUp: req_model.MFALevel(user.MFAMaxSetUp), + MFAInitSkipped: user.MFAInitSkipped, InitRequired: user.InitRequired, } } @@ -163,6 +192,25 @@ func UsersToModel(users []*UserView) []*model.UserView { return result } +func WebauthnTokensToModel(tokens []*WebAuthNView) []*model.WebAuthNView { + if tokens == nil { + return nil + } + result := make([]*model.WebAuthNView, len(tokens)) + for i, t := range tokens { + result[i] = WebauthnTokenToModel(t) + } + return result +} + +func WebauthnTokenToModel(token *WebAuthNView) *model.WebAuthNView { + return &model.WebAuthNView{ + TokenID: token.ID, + Name: token.Name, + State: model.MFAState(token.State), + } +} + func (u *UserView) GenerateLoginName(domain string, appendDomain bool) string { if !appendDomain { return u.UserName @@ -212,6 +260,12 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) { case es_model.UserPasswordChanged, es_model.HumanPasswordChanged: err = u.setPasswordData(event) + case es_model.HumanPasswordlessTokenAdded: + err = u.addPasswordlessToken(event) + case es_model.HumanPasswordlessTokenVerified: + err = u.updatePasswordlessToken(event) + case es_model.HumanPasswordlessTokenRemoved: + err = u.removePasswordlessToken(event) case es_model.UserProfileChanged, es_model.HumanProfileChanged, es_model.UserAddressChanged, @@ -251,17 +305,27 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) { u.State = int32(model.UserStateLocked) case es_model.MFAOTPAdded, es_model.HumanMFAOTPAdded: - u.OTPState = int32(model.MfaStateNotReady) + u.OTPState = int32(model.MFAStateNotReady) case es_model.MFAOTPVerified, es_model.HumanMFAOTPVerified: - u.OTPState = int32(model.MfaStateReady) - u.MfaInitSkipped = time.Time{} + u.OTPState = int32(model.MFAStateReady) + u.MFAInitSkipped = time.Time{} case es_model.MFAOTPRemoved, es_model.HumanMFAOTPRemoved: - u.OTPState = int32(model.MfaStateUnspecified) + u.OTPState = int32(model.MFAStateUnspecified) + case es_model.HumanMFAU2FTokenAdded: + err = u.addU2FToken(event) + case es_model.HumanMFAU2FTokenVerified: + err = u.updateU2FToken(event) + if err != nil { + return err + } + u.MFAInitSkipped = time.Time{} + case es_model.HumanMFAU2FTokenRemoved: + err = u.removeU2FToken(event) case es_model.MFAInitSkipped, es_model.HumanMFAInitSkipped: - u.MfaInitSkipped = event.CreationDate + u.MFAInitSkipped = event.CreationDate case es_model.InitializedUserCodeAdded, es_model.InitializedHumanCodeAdded: u.InitRequired = true @@ -298,6 +362,106 @@ func (u *UserView) setPasswordData(event *models.Event) error { return nil } +func (u *UserView) addPasswordlessToken(event *models.Event) error { + token, err := webAuthNViewFromEvent(event) + if err != nil { + return err + } + for _, t := range u.PasswordlessTokens { + if t.State == int32(model.MFAStateNotReady) { + t = token + return nil + } + } + u.U2FTokens = append(u.U2FTokens, token) + return nil +} + +func (u *UserView) updatePasswordlessToken(event *models.Event) error { + token, err := webAuthNViewFromEvent(event) + if err != nil { + return err + } + for i, t := range u.PasswordlessTokens { + if t.ID == token.ID { + u.PasswordlessTokens[i].Name = token.Name + u.PasswordlessTokens[i].State = int32(model.MFAStateReady) + return nil + } + } + return nil +} + +func (u *UserView) removePasswordlessToken(event *models.Event) error { + token, err := webAuthNViewFromEvent(event) + if err != nil { + return err + } + for i, t := range u.PasswordlessTokens { + if t.ID == token.ID { + u.PasswordlessTokens[i] = u.PasswordlessTokens[len(u.PasswordlessTokens)-1] + u.PasswordlessTokens[len(u.PasswordlessTokens)-1] = nil + u.PasswordlessTokens = u.PasswordlessTokens[:len(u.PasswordlessTokens)-1] + return nil + } + } + return nil +} + +func (u *UserView) addU2FToken(event *models.Event) error { + token, err := webAuthNViewFromEvent(event) + if err != nil { + return err + } + for _, t := range u.U2FTokens { + if t.State == int32(model.MFAStateNotReady) { + t = token + return nil + } + } + u.U2FTokens = append(u.U2FTokens, token) + return nil +} + +func (u *UserView) updateU2FToken(event *models.Event) error { + token, err := webAuthNViewFromEvent(event) + if err != nil { + return err + } + for i, t := range u.U2FTokens { + if t.ID == token.ID { + u.U2FTokens[i].Name = token.Name + u.U2FTokens[i].State = int32(model.MFAStateReady) + return nil + } + } + return nil +} + +func (u *UserView) removeU2FToken(event *models.Event) error { + token, err := webAuthNViewFromEvent(event) + if err != nil { + return err + } + for i := len(u.U2FTokens) - 1; i >= 0; i-- { + if u.U2FTokens[i].ID == token.ID { + u.U2FTokens[i] = u.U2FTokens[len(u.U2FTokens)-1] + u.U2FTokens[len(u.U2FTokens)-1] = nil + u.U2FTokens = u.U2FTokens[:len(u.U2FTokens)-1] + } + } + return nil +} + +func webAuthNViewFromEvent(event *models.Event) (*WebAuthNView, error) { + token := new(WebAuthNView) + err := json.Unmarshal(event.Data, token) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "MODEL-FSaq1", "could not unmarshal data") + } + return token, err +} + func (u *UserView) ComputeObject() { if !u.MachineView.IsZero() { if u.State == int32(model.UserStateUnspecified) { @@ -312,10 +476,25 @@ func (u *UserView) ComputeObject() { u.State = int32(model.UserStateInitial) } } - if u.OTPState != int32(model.MfaStateReady) { - u.MfaMaxSetUp = int32(req_model.MFALevelNotSetUp) - } - if u.OTPState == int32(model.MfaStateReady) { - u.MfaMaxSetUp = int32(req_model.MFALevelSecondFactor) - } + u.ComputeMFAMaxSetUp() +} + +func (u *UserView) ComputeMFAMaxSetUp() { + for _, token := range u.PasswordlessTokens { + if token.State == int32(model.MFAStateReady) { + u.MFAMaxSetUp = int32(req_model.MFALevelMultiFactor) + return + } + } + for _, token := range u.U2FTokens { + if token.State == int32(model.MFAStateReady) { + u.MFAMaxSetUp = int32(req_model.MFALevelSecondFactor) + return + } + } + if u.OTPState == int32(model.MFAStateReady) { + u.MFAMaxSetUp = int32(req_model.MFALevelSecondFactor) + return + } + u.MFAMaxSetUp = int32(req_model.MFALevelNotSetUp) } diff --git a/internal/user/repository/view/model/user_session.go b/internal/user/repository/view/model/user_session.go index e2f0f8c5c6..f213c3505c 100644 --- a/internal/user/repository/view/model/user_session.go +++ b/internal/user/repository/view/model/user_session.go @@ -32,6 +32,7 @@ type UserSessionView struct { DisplayName string `json:"-" gorm:"column:user_display_name"` SelectedIDPConfigID string `json:"selectedIDPConfigID" gorm:"column:selected_idp_config_id"` PasswordVerification time.Time `json:"-" gorm:"column:password_verification"` + PasswordlessVerification time.Time `json:"-" gorm:"column:passwordless_verification"` ExternalLoginVerification time.Time `json:"-" gorm:"column:external_login_verification"` SecondFactorVerification time.Time `json:"-" gorm:"column:second_factor_verification"` SecondFactorVerificationType int32 `json:"-" gorm:"column:second_factor_verification_type"` @@ -62,6 +63,7 @@ func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView { DisplayName: userSession.DisplayName, SelectedIDPConfigID: userSession.SelectedIDPConfigID, PasswordVerification: userSession.PasswordVerification, + PasswordlessVerification: userSession.PasswordlessVerification, ExternalLoginVerification: userSession.ExternalLoginVerification, SecondFactorVerification: userSession.SecondFactorVerification, SecondFactorVerificationType: req_model.MFAType(userSession.SecondFactorVerificationType), @@ -93,6 +95,15 @@ func (v *UserSessionView) AppendEvent(event *models.Event) { v.ExternalLoginVerification = event.CreationDate v.SelectedIDPConfigID = data.SelectedIDPConfigID v.State = int32(req_model.UserSessionStateActive) + case es_model.HumanPasswordlessTokenCheckSucceeded: + v.PasswordlessVerification = event.CreationDate + v.MultiFactorVerification = event.CreationDate + v.MultiFactorVerificationType = int32(req_model.MFATypeU2FUserVerification) + v.State = int32(req_model.UserSessionStateActive) + case es_model.HumanPasswordlessTokenCheckFailed, + es_model.HumanPasswordlessTokenRemoved: + v.PasswordlessVerification = time.Time{} + v.MultiFactorVerification = time.Time{} case es_model.UserPasswordCheckFailed, es_model.UserPasswordChanged, es_model.HumanPasswordCheckFailed, @@ -106,8 +117,14 @@ func (v *UserSessionView) AppendEvent(event *models.Event) { case es_model.MFAOTPCheckFailed, es_model.MFAOTPRemoved, es_model.HumanMFAOTPCheckFailed, - es_model.HumanMFAOTPRemoved: + es_model.HumanMFAOTPRemoved, + es_model.HumanMFAU2FTokenCheckFailed, + es_model.HumanMFAU2FTokenRemoved: v.SecondFactorVerification = time.Time{} + case es_model.HumanMFAU2FTokenCheckSucceeded: + v.SecondFactorVerification = event.CreationDate + v.SecondFactorVerificationType = int32(req_model.MFATypeU2F) + v.State = int32(req_model.UserSessionStateActive) case es_model.SignedOut, es_model.HumanSignedOut, es_model.UserLocked, diff --git a/internal/user/repository/view/model/user_test.go b/internal/user/repository/view/model/user_test.go index 853a1e55b0..41e91203cd 100644 --- a/internal/user/repository/view/model/user_test.go +++ b/internal/user/repository/view/model/user_test.go @@ -305,7 +305,7 @@ func TestUserAppendEvent(t *testing.T) { event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.MFAOTPAdded, ResourceOwner: "OrgID"}, user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateNotReady)}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)}, }, { name: "append human add otp event", @@ -313,39 +313,39 @@ func TestUserAppendEvent(t *testing.T) { event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.HumanMFAOTPAdded, ResourceOwner: "OrgID"}, user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateNotReady)}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)}, }, { name: "append user verify otp event", args: args{ event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.MFAOTPVerified, ResourceOwner: "OrgID"}, - user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateNotReady)}, State: int32(model.UserStateActive)}, + user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateReady)}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)}, }, { name: "append human verify otp event", args: args{ event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.HumanMFAOTPVerified, ResourceOwner: "OrgID"}, - user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateNotReady)}, State: int32(model.UserStateActive)}, + user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateReady)}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)}, }, { name: "append user remove otp event", args: args{ event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.MFAOTPRemoved, ResourceOwner: "OrgID"}, - user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateReady)}, State: int32(model.UserStateActive)}, + user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateUnspecified)}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateUnspecified)}, State: int32(model.UserStateActive)}, }, { name: "append human remove otp event", args: args{ event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.HumanMFAOTPRemoved, ResourceOwner: "OrgID"}, - user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateReady)}, State: int32(model.UserStateActive)}, + user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MfaStateUnspecified)}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateUnspecified)}, State: int32(model.UserStateActive)}, }, { name: "append user mfa init skipped event", @@ -353,7 +353,7 @@ func TestUserAppendEvent(t *testing.T) { event: &es_models.Event{Sequence: 1, CreationDate: time.Now().UTC(), Type: es_model.MFAInitSkipped, AggregateID: "AggregateID", ResourceOwner: "OrgID"}, user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MfaInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MFAInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)}, }, { name: "append human mfa init skipped event", @@ -361,7 +361,7 @@ func TestUserAppendEvent(t *testing.T) { event: &es_models.Event{Sequence: 1, CreationDate: time.Now().UTC(), Type: es_model.HumanMFAInitSkipped, AggregateID: "AggregateID", ResourceOwner: "OrgID"}, user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)}, }, - result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MfaInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)}, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MFAInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)}, }, } for _, tt := range tests { @@ -401,8 +401,8 @@ func TestUserAppendEvent(t *testing.T) { if human.OTPState != tt.result.OTPState { t.Errorf("got wrong result OTPState: expected: %v, actual: %v ", tt.result.OTPState, human.OTPState) } - if human.MfaInitSkipped.Round(1*time.Second) != tt.result.MfaInitSkipped.Round(1*time.Second) { - t.Errorf("got wrong result MfaInitSkipped: expected: %v, actual: %v ", tt.result.MfaInitSkipped.Round(1*time.Second), human.MfaInitSkipped.Round(1*time.Second)) + if human.MFAInitSkipped.Round(1*time.Second) != tt.result.MFAInitSkipped.Round(1*time.Second) { + t.Errorf("got wrong result MFAInitSkipped: expected: %v, actual: %v ", tt.result.MFAInitSkipped.Round(1*time.Second), human.MFAInitSkipped.Round(1*time.Second)) } if human.PasswordSet != tt.result.PasswordSet { t.Errorf("got wrong result PasswordSet: expected: %v, actual: %v ", tt.result.PasswordSet, human.PasswordSet) diff --git a/internal/user/repository/view/user_view.go b/internal/user/repository/view/user_view.go index 9ae31f354c..30edc95903 100644 --- a/internal/user/repository/view/user_view.go +++ b/internal/user/repository/view/user_view.go @@ -142,15 +142,15 @@ func IsUserUnique(db *gorm.DB, table, userName, email string) (bool, error) { return user.UserName == "", nil } -func UserMfas(db *gorm.DB, table, userID string) ([]*usr_model.MultiFactor, error) { +func UserMFAs(db *gorm.DB, table, userID string) ([]*usr_model.MultiFactor, error) { user, err := UserByID(db, table, userID) if err != nil { return nil, err } - if user.OTPState == int32(usr_model.MfaStateUnspecified) { + if user.OTPState == int32(usr_model.MFAStateUnspecified) { return []*usr_model.MultiFactor{}, nil } - return []*usr_model.MultiFactor{{Type: usr_model.MfaTypeOTP, State: usr_model.MfaState(user.OTPState)}}, nil + return []*usr_model.MultiFactor{{Type: usr_model.MFATypeOTP, State: usr_model.MFAState(user.OTPState)}}, nil } func PutUsers(db *gorm.DB, table string, users ...*model.UserView) error { diff --git a/internal/webauthn/converter.go b/internal/webauthn/converter.go new file mode 100644 index 0000000000..efe8b8bb26 --- /dev/null +++ b/internal/webauthn/converter.go @@ -0,0 +1,80 @@ +package webauthn + +import ( + "github.com/caos/zitadel/internal/user/model" + "github.com/duo-labs/webauthn/protocol" + "github.com/duo-labs/webauthn/webauthn" +) + +func WebAuthNsToCredentials(webAuthNs []*model.WebAuthNToken) []webauthn.Credential { + creds := make([]webauthn.Credential, 0) + for _, webAuthN := range webAuthNs { + if webAuthN.State == model.MFAStateReady { + creds = append(creds, webauthn.Credential{ + ID: webAuthN.KeyID, + PublicKey: webAuthN.PublicKey, + AttestationType: webAuthN.AttestationType, + Authenticator: webauthn.Authenticator{ + AAGUID: webAuthN.AAGUID, + SignCount: webAuthN.SignCount, + }, + }) + } + } + return creds +} + +func WebAuthNToSessionData(webAuthN *model.WebAuthNToken) webauthn.SessionData { + return webauthn.SessionData{ + Challenge: webAuthN.Challenge, + UserID: []byte(webAuthN.AggregateID), + AllowedCredentialIDs: webAuthN.AllowedCredentialIDs, + UserVerification: UserVerificationFromModel(webAuthN.UserVerification), + } +} + +func WebAuthNLoginToSessionData(webAuthN *model.WebAuthNLogin) webauthn.SessionData { + return webauthn.SessionData{ + Challenge: webAuthN.Challenge, + UserID: []byte(webAuthN.AggregateID), + AllowedCredentialIDs: webAuthN.AllowedCredentialIDs, + UserVerification: UserVerificationFromModel(webAuthN.UserVerification), + } +} + +func UserVerificationToModel(verification protocol.UserVerificationRequirement) model.UserVerificationRequirement { + switch verification { + case protocol.VerificationRequired: + return model.UserVerificationRequirementRequired + case protocol.VerificationPreferred: + return model.UserVerificationRequirementPreferred + case protocol.VerificationDiscouraged: + return model.UserVerificationRequirementDiscouraged + default: + return model.UserVerificationRequirementUnspecified + } +} + +func UserVerificationFromModel(verification model.UserVerificationRequirement) protocol.UserVerificationRequirement { + switch verification { + case model.UserVerificationRequirementRequired: + return protocol.VerificationRequired + case model.UserVerificationRequirementPreferred: + return protocol.VerificationPreferred + case model.UserVerificationRequirementDiscouraged: + return protocol.VerificationDiscouraged + default: + return protocol.VerificationDiscouraged + } +} + +func AuthenticatorAttachmentFromModel(authType model.AuthenticatorAttachment) protocol.AuthenticatorAttachment { + switch authType { + case model.AuthenticatorAttachmentPlattform: + return protocol.Platform + case model.AuthenticatorAttachmentCrossPlattform: + return protocol.CrossPlatform + default: + return "" + } +} diff --git a/internal/webauthn/webauthn.go b/internal/webauthn/webauthn.go new file mode 100644 index 0000000000..62af99ff38 --- /dev/null +++ b/internal/webauthn/webauthn.go @@ -0,0 +1,158 @@ +package webauthn + +import ( + "bytes" + "encoding/json" + + "github.com/duo-labs/webauthn/protocol" + "github.com/duo-labs/webauthn/webauthn" + + caos_errs "github.com/caos/zitadel/internal/errors" + usr_model "github.com/caos/zitadel/internal/user/model" +) + +type WebAuthN struct { + web *webauthn.WebAuthn +} + +func StartServer(displayName, id, origin string) (*WebAuthN, error) { + web, err := webauthn.New(&webauthn.Config{ + RPDisplayName: displayName, + RPID: id, + RPOrigin: origin, + }) + if err != nil { + return nil, err + } + return &WebAuthN{ + web: web, + }, err +} + +type webUser struct { + *usr_model.User + credentials []webauthn.Credential +} + +func (u *webUser) WebAuthnID() []byte { + return []byte(u.AggregateID) +} + +func (u *webUser) WebAuthnName() string { + return u.UserName +} + +func (u *webUser) WebAuthnDisplayName() string { + return u.DisplayName +} + +func (u *webUser) WebAuthnIcon() string { + return "" +} + +func (u *webUser) WebAuthnCredentials() []webauthn.Credential { + return u.credentials +} + +func (w *WebAuthN) BeginRegistration(user *usr_model.User, authType usr_model.AuthenticatorAttachment, userVerification usr_model.UserVerificationRequirement, webAuthNs ...*usr_model.WebAuthNToken) (*usr_model.WebAuthNToken, error) { + creds := WebAuthNsToCredentials(webAuthNs) + existing := make([]protocol.CredentialDescriptor, len(creds)) + for i, cred := range creds { + existing[i] = protocol.CredentialDescriptor{ + Type: protocol.PublicKeyCredentialType, + CredentialID: cred.ID, + } + } + credentialOptions, sessionData, err := w.web.BeginRegistration( + &webUser{ + User: user, + credentials: creds, + }, + webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ + UserVerification: UserVerificationFromModel(userVerification), + AuthenticatorAttachment: AuthenticatorAttachmentFromModel(authType), + }), + webauthn.WithConveyancePreference(protocol.PreferNoAttestation), + webauthn.WithExclusions(existing), + ) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "WEBAU-bM8sd", "Errors.User.WebAuthN.BeginRegisterFailed") + } + cred, err := json.Marshal(credentialOptions) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "WEBAU-D7cus", "Errors.User.WebAuthN.MarshalError") + } + return &usr_model.WebAuthNToken{ + Challenge: sessionData.Challenge, + CredentialCreationData: cred, + AllowedCredentialIDs: sessionData.AllowedCredentialIDs, + UserVerification: UserVerificationToModel(sessionData.UserVerification), + }, nil +} + +func (w *WebAuthN) FinishRegistration(user *usr_model.User, webAuthN *usr_model.WebAuthNToken, tokenName string, credData []byte) (*usr_model.WebAuthNToken, error) { + if webAuthN == nil { + return nil, caos_errs.ThrowInternal(nil, "WEBAU-5M9so", "Errors.User.WebAuthN.NotFound") + } + credentialData, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader(credData)) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "WEBAU-sEr8c", "Errors.User.WebAuthN.ErrorOnParseCredential") + } + sessionData := WebAuthNToSessionData(webAuthN) + credential, err := w.web.CreateCredential( + &webUser{ + User: user, + }, + sessionData, credentialData) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "WEBAU-3Vb9s", "Errors.User.WebAuthN.CreateCredentialFailed") + } + + webAuthN.KeyID = credential.ID + webAuthN.PublicKey = credential.PublicKey + webAuthN.AttestationType = credential.AttestationType + webAuthN.AAGUID = credential.Authenticator.AAGUID + webAuthN.SignCount = credential.Authenticator.SignCount + webAuthN.WebAuthNTokenName = tokenName + return webAuthN, nil +} + +func (w *WebAuthN) BeginLogin(user *usr_model.User, userVerification usr_model.UserVerificationRequirement, webAuthNs ...*usr_model.WebAuthNToken) (*usr_model.WebAuthNLogin, error) { + assertion, sessionData, err := w.web.BeginLogin(&webUser{ + User: user, + credentials: WebAuthNsToCredentials(webAuthNs), + }, webauthn.WithUserVerification(UserVerificationFromModel(userVerification))) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "WEBAU-4G8sw", "Errors.User.WebAuthN.BeginLoginFailed") + } + cred, err := json.Marshal(assertion) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "WEBAU-2M0s9", "Errors.User.WebAuthN.MarshalError") + } + return &usr_model.WebAuthNLogin{ + Challenge: sessionData.Challenge, + CredentialAssertionData: cred, + AllowedCredentialIDs: sessionData.AllowedCredentialIDs, + UserVerification: userVerification, + }, nil +} + +func (w *WebAuthN) FinishLogin(user *usr_model.User, webAuthN *usr_model.WebAuthNLogin, credData []byte, webAuthNs ...*usr_model.WebAuthNToken) ([]byte, uint32, error) { + assertionData, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(credData)) + if err != nil { + return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-ADgv4", "Errors.User.WebAuthN.ValidateLoginFailed") + } + webUser := &webUser{ + User: user, + credentials: WebAuthNsToCredentials(webAuthNs), + } + credential, err := w.web.ValidateLogin(webUser, WebAuthNLoginToSessionData(webAuthN), assertionData) + if err != nil { + return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-3M9si", "Errors.User.WebAuthN.ValidateLoginFailed") + } + + if credential.Authenticator.CloneWarning { + return credential.ID, credential.Authenticator.SignCount, caos_errs.ThrowInternal(err, "WEBAU-4M90s", "Errors.User.WebAuthN.CloneWarning") + } + return credential.ID, credential.Authenticator.SignCount, nil +} diff --git a/migrations/cockroach/V1.25__webauthn.sql b/migrations/cockroach/V1.25__webauthn.sql new file mode 100644 index 0000000000..67787c7c1f --- /dev/null +++ b/migrations/cockroach/V1.25__webauthn.sql @@ -0,0 +1,13 @@ +ALTER TABLE management.login_policies ADD COLUMN passwordless_type SMALLINT; +ALTER TABLE adminapi.login_policies ADD COLUMN passwordless_type SMALLINT; +ALTER TABLE auth.login_policies ADD COLUMN passwordless_type SMALLINT; + +ALTER TABLE management.users ADD COLUMN u2f_tokens BYTEA; +ALTER TABLE auth.users ADD COLUMN u2f_tokens BYTEA; +ALTER TABLE adminapi.users ADD COLUMN u2f_tokens BYTEA; + +ALTER TABLE management.users ADD COLUMN passwordless_tokens BYTEA; +ALTER TABLE auth.users ADD COLUMN passwordless_tokens BYTEA; +ALTER TABLE adminapi.users ADD COLUMN passwordless_tokens BYTEA; + +ALTER TABLE auth.user_sessions ADD COLUMN passwordless_verification TIMESTAMPTZ; diff --git a/pkg/grpc/admin/proto/admin.proto b/pkg/grpc/admin/proto/admin.proto index fa4677c1cc..d0c44df3e6 100644 --- a/pkg/grpc/admin/proto/admin.proto +++ b/pkg/grpc/admin/proto/admin.proto @@ -999,6 +999,7 @@ message DefaultLoginPolicy { google.protobuf.Timestamp creation_date = 4; google.protobuf.Timestamp change_date = 5; bool force_mfa = 6; + PasswordlessType passwordless_type = 7; } message DefaultLoginPolicyRequest { @@ -1006,6 +1007,12 @@ message DefaultLoginPolicyRequest { bool allow_register = 2; bool allow_external_idp = 3; bool force_mfa = 4; + PasswordlessType passwordless_type = 5; +} + +enum PasswordlessType { + PASSWORDLESSTYPE_NOT_ALLOWED = 0; + PASSWORDLESSTYPE_ALLOWED = 1; } message IdpProviderID { @@ -1019,6 +1026,7 @@ message DefaultLoginPolicyView { google.protobuf.Timestamp creation_date = 4; google.protobuf.Timestamp change_date = 5; bool force_mfa = 6; + PasswordlessType passwordless_type = 7; } message IdpProviderView { diff --git a/pkg/grpc/auth/mock/auth.proto.mock.go b/pkg/grpc/auth/mock/auth.proto.mock.go index 22f851fa4c..d7ca8c5e4a 100644 --- a/pkg/grpc/auth/mock/auth.proto.mock.go +++ b/pkg/grpc/auth/mock/auth.proto.mock.go @@ -56,6 +56,46 @@ func (mr *MockAuthServiceClientMockRecorder) AddMfaOTP(arg0, arg1 interface{}, a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMfaOTP", reflect.TypeOf((*MockAuthServiceClient)(nil).AddMfaOTP), varargs...) } +// AddMyMfaU2F mocks base method +func (m *MockAuthServiceClient) AddMyMfaU2F(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*auth.WebAuthNResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AddMyMfaU2F", varargs...) + ret0, _ := ret[0].(*auth.WebAuthNResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddMyMfaU2F indicates an expected call of AddMyMfaU2F +func (mr *MockAuthServiceClientMockRecorder) AddMyMfaU2F(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMyMfaU2F", reflect.TypeOf((*MockAuthServiceClient)(nil).AddMyMfaU2F), varargs...) +} + +// AddMyPasswordless mocks base method +func (m *MockAuthServiceClient) AddMyPasswordless(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*auth.WebAuthNResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AddMyPasswordless", varargs...) + ret0, _ := ret[0].(*auth.WebAuthNResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddMyPasswordless indicates an expected call of AddMyPasswordless +func (mr *MockAuthServiceClientMockRecorder) AddMyPasswordless(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMyPasswordless", reflect.TypeOf((*MockAuthServiceClient)(nil).AddMyPasswordless), varargs...) +} + // ChangeMyPassword mocks base method func (m *MockAuthServiceClient) ChangeMyPassword(arg0 context.Context, arg1 *auth.PasswordChange, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() @@ -416,6 +456,46 @@ func (mr *MockAuthServiceClientMockRecorder) RemoveMyExternalIDP(arg0, arg1 inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveMyExternalIDP", reflect.TypeOf((*MockAuthServiceClient)(nil).RemoveMyExternalIDP), varargs...) } +// RemoveMyMfaU2F mocks base method +func (m *MockAuthServiceClient) RemoveMyMfaU2F(arg0 context.Context, arg1 *auth.WebAuthNTokenID, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveMyMfaU2F", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveMyMfaU2F indicates an expected call of RemoveMyMfaU2F +func (mr *MockAuthServiceClientMockRecorder) RemoveMyMfaU2F(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveMyMfaU2F", reflect.TypeOf((*MockAuthServiceClient)(nil).RemoveMyMfaU2F), varargs...) +} + +// RemoveMyPasswordless mocks base method +func (m *MockAuthServiceClient) RemoveMyPasswordless(arg0 context.Context, arg1 *auth.WebAuthNTokenID, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveMyPasswordless", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveMyPasswordless indicates an expected call of RemoveMyPasswordless +func (mr *MockAuthServiceClientMockRecorder) RemoveMyPasswordless(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveMyPasswordless", reflect.TypeOf((*MockAuthServiceClient)(nil).RemoveMyPasswordless), varargs...) +} + // RemoveMyUserPhone mocks base method func (m *MockAuthServiceClient) RemoveMyUserPhone(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() @@ -596,6 +676,46 @@ func (mr *MockAuthServiceClientMockRecorder) VerifyMfaOTP(arg0, arg1 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyMfaOTP", reflect.TypeOf((*MockAuthServiceClient)(nil).VerifyMfaOTP), varargs...) } +// VerifyMyMfaU2F mocks base method +func (m *MockAuthServiceClient) VerifyMyMfaU2F(arg0 context.Context, arg1 *auth.VerifyWebAuthN, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "VerifyMyMfaU2F", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VerifyMyMfaU2F indicates an expected call of VerifyMyMfaU2F +func (mr *MockAuthServiceClientMockRecorder) VerifyMyMfaU2F(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyMyMfaU2F", reflect.TypeOf((*MockAuthServiceClient)(nil).VerifyMyMfaU2F), varargs...) +} + +// VerifyMyPasswordless mocks base method +func (m *MockAuthServiceClient) VerifyMyPasswordless(arg0 context.Context, arg1 *auth.VerifyWebAuthN, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "VerifyMyPasswordless", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VerifyMyPasswordless indicates an expected call of VerifyMyPasswordless +func (mr *MockAuthServiceClientMockRecorder) VerifyMyPasswordless(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyMyPasswordless", reflect.TypeOf((*MockAuthServiceClient)(nil).VerifyMyPasswordless), varargs...) +} + // VerifyMyUserEmail mocks base method func (m *MockAuthServiceClient) VerifyMyUserEmail(arg0 context.Context, arg1 *auth.VerifyMyUserEmailRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() diff --git a/pkg/grpc/auth/proto/auth.proto b/pkg/grpc/auth/proto/auth.proto index 723a614f86..fccedd0b55 100644 --- a/pkg/grpc/auth/proto/auth.proto +++ b/pkg/grpc/auth/proto/auth.proto @@ -244,12 +244,12 @@ service AuthService { rpc GetMyPasswordComplexityPolicy(google.protobuf.Empty) returns (PasswordComplexityPolicy) { option (google.api.http) = { - get: "/policies/passwords/complexity" - }; + get: "/policies/passwords/complexity" + }; option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; + permission: "authenticated" + }; } //ExternalIDP @@ -306,6 +306,68 @@ service AuthService { }; } + rpc AddMyMfaU2F(google.protobuf.Empty) returns (WebAuthNResponse) { + option (google.api.http) = { + post: "/users/me/mfas/u2f" + body: "*" + }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + + rpc VerifyMyMfaU2F(VerifyWebAuthN) returns (google.protobuf.Empty) { + option (google.api.http) = { + put: "/users/me/mfas/u2f/_verify" + body: "*" + }; + + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + + rpc RemoveMyMfaU2F(WebAuthNTokenID) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/users/me/mfas/u2f/{id}" + }; + + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + + rpc AddMyPasswordless(google.protobuf.Empty) returns (WebAuthNResponse) { + option (google.api.http) = { + post: "/users/me/passwordless" + body: "*" + }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + + rpc VerifyMyPasswordless(VerifyWebAuthN) returns (google.protobuf.Empty) { + option (google.api.http) = { + put: "/users/me/passwordless/_verify" + body: "*" + }; + + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + + rpc RemoveMyPasswordless(WebAuthNTokenID) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/users/me/passwordless/{id}" + }; + + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + rpc SearchMyUserGrant(UserGrantSearchRequest) returns (UserGrantSearchResponse) { option (google.api.http) = { post: "/usergrants/me/_search" @@ -578,8 +640,8 @@ message PasswordChange { enum MfaType { MFATYPE_UNSPECIFIED = 0; - MFATYPE_SMS = 1; - MFATYPE_OTP = 2; + MFATYPE_OTP = 1; + MFATYPE_U2F = 2; } message VerifyMfaOtp { @@ -593,6 +655,7 @@ message MultiFactors { message MultiFactor { MfaType type = 1; MFAState state = 2; + string attribute = 3; } message MfaOtpResponse { @@ -602,6 +665,21 @@ message MfaOtpResponse { MFAState state = 4; } +message WebAuthNResponse { + string id = 1; + bytes public_key = 2; + MFAState state = 3; +} + +message VerifyWebAuthN { + bytes public_key_credential = 1; + string token_name = 2; +} + +message WebAuthNTokenID { + string id = 1; +} + enum MFAState { MFASTATE_UNSPECIFIED = 0; MFASTATE_NOT_READY = 1; @@ -691,7 +769,7 @@ enum SearchMethod { } message ChangesRequest { - uint64 limit= 1; + uint64 limit = 1; uint64 sequence_offset = 2; bool asc = 3; } diff --git a/pkg/grpc/management/proto/management.proto b/pkg/grpc/management/proto/management.proto index d4184d9eb4..94d5564652 100644 --- a/pkg/grpc/management/proto/management.proto +++ b/pkg/grpc/management/proto/management.proto @@ -2033,8 +2033,8 @@ message UserMultiFactor { enum MfaType { MFATYPE_UNSPECIFIED = 0; - MFATYPE_SMS = 1; - MFATYPE_OTP = 2; + MFATYPE_OTP = 1; + MFATYPE_U2F = 2; } enum MFAState { @@ -3064,6 +3064,7 @@ message LoginPolicy { google.protobuf.Timestamp creation_date = 4; google.protobuf.Timestamp change_date = 5; bool force_mfa = 6; + PasswordlessType passwordless_type = 7; } message LoginPolicyRequest { @@ -3071,6 +3072,12 @@ message LoginPolicyRequest { bool allow_register = 2; bool allow_external_idp = 3; bool force_mfa = 4; + PasswordlessType passwordless_type = 5; +} + +enum PasswordlessType { + PASSWORDLESSTYPE_NOT_ALLOWED = 0; + PASSWORDLESSTYPE_ALLOWED = 1; } message IdpProviderID { @@ -3095,6 +3102,7 @@ message LoginPolicyView { google.protobuf.Timestamp creation_date = 5; google.protobuf.Timestamp change_date = 6; bool force_mfa = 7; + PasswordlessType passwordless_type = 8; } message IdpProviderView {