feat: add WebAuthN support for passwordless login and 2fa (#966)

* at least registration prompt works

* in memory test for login

* buttons to start webauthn process

* begin eventstore impl

* begin eventstore impl

* serialize into bytes

* fix: u2f, passwordless types

* fix for localhost

* fix script

* fix: u2f, passwordless types

* fix: add u2f

* fix: verify u2f

* fix: session data in event store

* fix: u2f credentials in eventstore

* fix: webauthn pkg handles business models

* feat: tests

* feat: append events

* fix: test

* fix: check only ready webauthn creds

* fix: move u2f methods to authrepo

* frontend improvements

* fix return

* feat: add passwordless

* feat: add passwordless

* improve ui / error handling

* separate call for login

* fix login

* js

* feat: u2f login methods

* feat: remove unused session id

* feat: error handling

* feat: error handling

* feat: refactor user eventstore

* feat: finish webauthn

* feat: u2f and passwordlss in auth.proto

* u2f step

* passwordless step

* cleanup js

* EndpointPasswordLessLogin

* migration

* update mfaChecked test

* next step test

* token name

* cleanup

* attribute

* passwordless as tokens

* remove sms as otp type

* add "user" to amr for webauthn

* error handling

* fixes

* fix tests

* naming

* naming

* fixes

* session handler

* i18n

* error handling in login

* Update internal/ui/login/static/i18n/de.yaml

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* Update internal/ui/login/static/i18n/en.yaml

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* improvements

* merge fixes

* fixes

* fixes

Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Livio Amstutz 2020-12-02 17:00:04 +01:00 committed by GitHub
parent 184e79be97
commit 300ade66a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 3383 additions and 740 deletions

View File

@ -97,3 +97,5 @@ SetUp:
SecondaryColor: '#ffffff' SecondaryColor: '#ffffff'
Step7: Step7:
DefaultSecondFactor: 1 #SecondFactorTypeOTP DefaultSecondFactor: 1 #SecondFactorTypeOTP
Step8:
DefaultSecondFactor: 2 #SecondFactorTypeU2F

View File

@ -53,7 +53,7 @@ SystemDefaults:
VerificationLifetimes: VerificationLifetimes:
PasswordCheck: 240h #10d PasswordCheck: 240h #10d
ExternalLoginCheck: 240h #10d ExternalLoginCheck: 240h #10d
MfaInitSkip: 720h #30d MFAInitSkip: 720h #30d
SecondFactorCheck: 18h SecondFactorCheck: 18h
MultiFactorCheck: 12h MultiFactorCheck: 12h
IamID: 'IAM' IamID: 'IAM'
@ -125,3 +125,7 @@ SystemDefaults:
Greeting: 'DomainClaimed.Greeting' Greeting: 'DomainClaimed.Greeting'
Text: 'DomainClaimed.Text' Text: 'DomainClaimed.Text'
ButtonText: 'DomainClaimed.ButtonText' ButtonText: 'DomainClaimed.ButtonText'
WebAuthN:
ID: $ZITADEL_COOKIE_DOMAIN
Origin: $ZITADEL_ACCOUNTS
DisplayName: ZITADEL

View File

@ -164,8 +164,8 @@
"OTP_DIALOG_DESCRIPTION": "Scanne den QR-Code mit einer Authenticator App und verifiziere den erhaltenen Code, um OTP zu aktivieren.", "OTP_DIALOG_DESCRIPTION": "Scanne den QR-Code mit einer Authenticator App und verifiziere den erhaltenen Code, um OTP zu aktivieren.",
"TYPE": { "TYPE": {
"0":"Keine MFA definiert", "0":"Keine MFA definiert",
"1":"SMS", "1":"OTP",
"2":"OTP" "2":"U2F"
}, },
"STATE": { "STATE": {
"0": "Kein Status", "0": "Kein Status",

View File

@ -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.", "OTP_DIALOG_DESCRIPTION": "Scan the QR code with an authenticator app and enter the code below to verify and activate the OTP method.",
"TYPE": { "TYPE": {
"0": "No MFA defined", "0": "No MFA defined",
"1": "SMS", "1": "OTP",
"2": "OTP" "2": "U2F"
}, },
"STATE": { "STATE": {
"0": "No State", "0": "No State",

1
go.mod
View File

@ -17,6 +17,7 @@ require (
github.com/caos/logging v0.0.2 github.com/caos/logging v0.0.2
github.com/caos/oidc v0.13.1 github.com/caos/oidc v0.13.1
github.com/cockroachdb/cockroach-go/v2 v2.0.8 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/envoyproxy/protoc-gen-validate v0.4.1
github.com/ghodss/yaml v1.0.0 github.com/ghodss/yaml v1.0.0
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b

12
go.sum
View File

@ -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/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 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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 h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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= 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/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 h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 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/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/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-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= 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/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/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/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 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 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= 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 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 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 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.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.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/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/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 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 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/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 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 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/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.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 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/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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@ -92,6 +92,12 @@ func (u *User) ProcessUser(event *models.Event) (err error) {
es_model.HumanMFAOTPAdded, es_model.HumanMFAOTPAdded,
es_model.HumanMFAOTPVerified, es_model.HumanMFAOTPVerified,
es_model.HumanMFAOTPRemoved, es_model.HumanMFAOTPRemoved,
es_model.HumanMFAU2FTokenAdded,
es_model.HumanMFAU2FTokenVerified,
es_model.HumanMFAU2FTokenRemoved,
es_model.HumanPasswordlessTokenAdded,
es_model.HumanPasswordlessTokenVerified,
es_model.HumanPasswordlessTokenRemoved,
es_model.MachineChanged: es_model.MachineChanged:
user, err = u.view.UserByID(event.AggregateID) user, err = u.view.UserByID(event.AggregateID)
if err != nil { if err != nil {

View File

@ -36,8 +36,8 @@ func (v *View) IsUserUnique(userName, email string) (bool, error) {
return view.IsUserUnique(v.Db, userTable, userName, email) return view.IsUserUnique(v.Db, userTable, userName, email)
} }
func (v *View) UserMfas(userID string) ([]*usr_model.MultiFactor, error) { func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) {
return view.UserMfas(v.Db, userTable, userID) return view.UserMFAs(v.Db, userTable, userID)
} }
func (v *View) PutUsers(user []*model.UserView, sequence uint64, eventTimestamp time.Time) error { func (v *View) PutUsers(user []*model.UserView, sequence uint64, eventTimestamp time.Time) error {

View File

@ -13,6 +13,7 @@ func loginPolicyToModel(policy *admin.DefaultLoginPolicyRequest) *iam_model.Logi
AllowExternalIdp: policy.AllowExternalIdp, AllowExternalIdp: policy.AllowExternalIdp,
AllowRegister: policy.AllowRegister, AllowRegister: policy.AllowRegister,
ForceMFA: policy.ForceMfa, ForceMFA: policy.ForceMfa,
PasswordlessType: passwordlessTypeToModel(policy.PasswordlessType),
} }
} }
@ -28,6 +29,7 @@ func loginPolicyFromModel(policy *iam_model.LoginPolicy) *admin.DefaultLoginPoli
AllowExternalIdp: policy.AllowExternalIdp, AllowExternalIdp: policy.AllowExternalIdp,
AllowRegister: policy.AllowRegister, AllowRegister: policy.AllowRegister,
ForceMfa: policy.ForceMFA, ForceMfa: policy.ForceMFA,
PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType),
CreationDate: creationDate, CreationDate: creationDate,
ChangeDate: changeDate, ChangeDate: changeDate,
} }
@ -45,6 +47,7 @@ func loginPolicyViewFromModel(policy *iam_model.LoginPolicyView) *admin.DefaultL
AllowExternalIdp: policy.AllowExternalIDP, AllowExternalIdp: policy.AllowExternalIDP,
AllowRegister: policy.AllowRegister, AllowRegister: policy.AllowRegister,
ForceMfa: policy.ForceMFA, ForceMfa: policy.ForceMFA,
PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType),
CreationDate: creationDate, CreationDate: creationDate,
ChangeDate: changeDate, 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 { func multiFactorResultFromModel(result *iam_model.MultiFactorsSearchResponse) *admin.MultiFactorsResult {
converted := make([]admin.MultiFactorType, len(result.Result)) converted := make([]admin.MultiFactorType, len(result.Result))
for i, mfaType := range result.Result { for i, mfaType := range result.Result {

View File

@ -2,7 +2,6 @@ package auth
import ( import (
"context" "context"
"github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/empty"
"github.com/caos/zitadel/pkg/grpc/auth" "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) { 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 { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err 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) { 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 return &empty.Empty{}, err
} }
func (s *Server) RemoveMfaOTP(ctx context.Context, _ *empty.Empty) (_ *empty.Empty, err error) { 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 return &empty.Empty{}, err
} }

View File

@ -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 { switch state {
case usr_model.MfaStateReady: case usr_model.MFAStateReady:
return auth.MFAState_MFASTATE_READY return auth.MFAState_MFASTATE_READY
case usr_model.MfaStateNotReady: case usr_model.MFAStateNotReady:
return auth.MFAState_MFASTATE_NOT_READY return auth.MFAState_MFASTATE_NOT_READY
default: default:
return auth.MFAState_MFASTATE_UNSPECIFIED 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 { func mfaFromModel(mfa *usr_model.MultiFactor) *auth.MultiFactor {
return &auth.MultiFactor{ return &auth.MultiFactor{
State: mfaStateFromModel(mfa.State), State: mfaStateFromModel(mfa.State),
Type: mfaTypeFromModel(mfa.Type), Type: mfaTypeFromModel(mfa.Type),
Attribute: mfa.Attribute,
} }
} }
func mfaTypeFromModel(mfatype usr_model.MfaType) auth.MfaType { func mfaTypeFromModel(mfaType usr_model.MFAType) auth.MfaType {
switch mfatype { switch mfaType {
case usr_model.MfaTypeOTP: case usr_model.MFATypeOTP:
return auth.MfaType_MFATYPE_OTP return auth.MfaType_MFATYPE_OTP
case usr_model.MfaTypeSMS: case usr_model.MFATypeU2F:
return auth.MfaType_MFATYPE_SMS return auth.MfaType_MFATYPE_U2F
default: default:
return auth.MfaType_MFATYPE_UNSPECIFIED return auth.MfaType_MFATYPE_UNSPECIFIED
} }
@ -426,3 +427,11 @@ func userChangesToAPI(changes *usr_model.UserChanges) (_ []*auth.Change) {
return result return result
} }
func verifyWebAuthNFromModel(u2f *usr_model.WebAuthNToken) *auth.WebAuthNResponse {
return &auth.WebAuthNResponse{
Id: u2f.WebAuthNTokenID,
PublicKey: u2f.PublicKey,
State: mfaStateFromModel(u2f.State),
}
}

View File

@ -13,6 +13,7 @@ func loginPolicyRequestToModel(policy *management.LoginPolicyRequest) *iam_model
AllowExternalIdp: policy.AllowExternalIdp, AllowExternalIdp: policy.AllowExternalIdp,
AllowRegister: policy.AllowRegister, AllowRegister: policy.AllowRegister,
ForceMFA: policy.ForceMfa, ForceMFA: policy.ForceMfa,
PasswordlessType: passwordlessTypeToModel(policy.PasswordlessType),
} }
} }
@ -30,6 +31,7 @@ func loginPolicyFromModel(policy *iam_model.LoginPolicy) *management.LoginPolicy
CreationDate: creationDate, CreationDate: creationDate,
ChangeDate: changeDate, ChangeDate: changeDate,
ForceMfa: policy.ForceMFA, ForceMfa: policy.ForceMFA,
PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType),
} }
} }
@ -48,6 +50,7 @@ func loginPolicyViewFromModel(policy *iam_model.LoginPolicyView) *management.Log
CreationDate: creationDate, CreationDate: creationDate,
ChangeDate: changeDate, ChangeDate: changeDate,
ForceMfa: policy.ForceMFA, ForceMfa: policy.ForceMFA,
PasswordlessType: passwordlessTypeFromModel(policy.PasswordlessType),
} }
} }
@ -215,3 +218,21 @@ func multiFactorTypeToModel(mfaType *management.MultiFactor) iam_model.MultiFact
return iam_model.MultiFactorTypeUnspecified 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
}
}

View File

@ -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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -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 { switch mfatype {
case usr_model.MfaTypeOTP: case usr_model.MFATypeOTP:
return management.MfaType_MFATYPE_OTP return management.MfaType_MFATYPE_OTP
case usr_model.MfaTypeSMS: case usr_model.MFATypeU2F:
return management.MfaType_MFATYPE_SMS return management.MfaType_MFATYPE_U2F
default: default:
return management.MfaType_MFATYPE_UNSPECIFIED return management.MfaType_MFATYPE_UNSPECIFIED
} }
} }
func mfaStateFromModel(state usr_model.MfaState) management.MFAState { func mfaStateFromModel(state usr_model.MFAState) management.MFAState {
switch state { switch state {
case usr_model.MfaStateReady: case usr_model.MFAStateReady:
return management.MFAState_MFASTATE_READY return management.MFAState_MFASTATE_READY
case usr_model.MfaStateNotReady: case usr_model.MFAStateNotReady:
return management.MFAState_MFASTATE_NOT_READY return management.MFAState_MFASTATE_NOT_READY
default: default:
return management.MFAState_MFASTATE_UNSPECIFIED return management.MFAState_MFASTATE_UNSPECIFIED

View File

@ -15,9 +15,10 @@ import (
) )
const ( const (
amrPassword = "password" amrPassword = "password"
amrMFA = "mfa" amrMFA = "mfa"
amrOTP = "otp" amrOTP = "otp"
amrUserPresence = "user"
) )
type AuthRequest struct { type AuthRequest struct {
@ -38,11 +39,11 @@ func (a *AuthRequest) GetAMR() []string {
if a.PasswordVerified { if a.PasswordVerified {
amr = append(amr, amrPassword) amr = append(amr, amrPassword)
} }
if len(a.MfasVerified) > 0 { if len(a.MFAsVerified) > 0 {
amr = append(amr, amrMFA) amr = append(amr, amrMFA)
for _, mfa := range a.MfasVerified { for _, mfa := range a.MFAsVerified {
if amrMfa := AMRFromMFAType(mfa); amrMfa != "" { if amrMFA := AMRFromMFAType(mfa); amrMFA != "" {
amr = append(amr, amrMfa) amr = append(amr, amrMFA)
} }
} }
} }
@ -247,6 +248,9 @@ func AMRFromMFAType(mfaType model.MFAType) string {
switch mfaType { switch mfaType {
case model.MFATypeOTP: case model.MFATypeOTP:
return amrOTP return amrOTP
case model.MFATypeU2F,
model.MFATypeU2FUserVerification:
return amrUserPresence
default: default:
return "" return ""
} }

View File

@ -2,10 +2,9 @@ package repository
import ( import (
"context" "context"
"github.com/caos/zitadel/internal/auth_request/model"
org_model "github.com/caos/zitadel/internal/org/model" org_model "github.com/caos/zitadel/internal/org/model"
user_model "github.com/caos/zitadel/internal/user/model" user_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/auth_request/model"
) )
type AuthRequestRepository interface { type AuthRequestRepository interface {
@ -15,14 +14,22 @@ type AuthRequestRepository interface {
AuthRequestByCode(ctx context.Context, code string) (*model.AuthRequest, error) AuthRequestByCode(ctx context.Context, code string) (*model.AuthRequest, error)
SaveAuthCode(ctx context.Context, id, code, userAgentID string) error SaveAuthCode(ctx context.Context, id, code, userAgentID string) error
DeleteAuthRequest(ctx context.Context, id string) error DeleteAuthRequest(ctx context.Context, id string) error
CheckLoginName(ctx context.Context, id, loginName, userAgentID 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 CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *model.ExternalUser, info *model.BrowserInfo) error
SelectUser(ctx context.Context, id, userID, userAgentID string) error SelectUser(ctx context.Context, id, userID, userAgentID string) error
SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, 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 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 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 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 ResetLinkingUsers(ctx context.Context, authReqID, userAgentID string) error
GetOrgByPrimaryDomain(primaryDomain string) (*org_model.OrgView, error) GetOrgByPrimaryDomain(primaryDomain string) (*org_model.OrgView, error)
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/caos/zitadel/internal/project/model" "github.com/caos/zitadel/internal/project/model"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing" proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model" proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model"
"github.com/caos/zitadel/internal/telemetry/tracing"
) )
type ApplicationRepo struct { type ApplicationRepo struct {
@ -22,7 +23,10 @@ func (a *ApplicationRepo) ApplicationByClientID(ctx context.Context, clientID st
return proj_view_model.ApplicationViewToModel(app), nil 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) app, err := a.View.ApplicationByClientID(ctx, clientID)
if err != nil { if err != nil {
return err return err

View File

@ -47,7 +47,7 @@ type AuthRequestRepo struct {
PasswordCheckLifeTime time.Duration PasswordCheckLifeTime time.Duration
ExternalLoginCheckLifeTime time.Duration ExternalLoginCheckLifeTime time.Duration
MfaInitSkippedLifeTime time.Duration MFAInitSkippedLifeTime time.Duration
SecondFactorCheckLifeTime time.Duration SecondFactorCheckLifeTime time.Duration
MultiFactorCheckLifeTime 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) { func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, password, userAgentID string, info *model.BrowserInfo) (err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequest(ctx, id, userAgentID) request, err := repo.getAuthRequestEnsureUser(ctx, id, userAgentID, userID)
if err != nil { if err != nil {
return err 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)) 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) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequest(ctx, authRequestID, userAgentID) request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
if err != nil { if err != nil {
return err return err
} }
if request.UserID != userID { return repo.UserEvents.CheckMFAOTP(ctx, userID, code, request.WithCurrentInfo(info))
return errors.ThrowPreconditionFailed(nil, "EVENT-ADJ26", "Errors.User.NotMatchingUserID") }
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) { 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 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) { func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id, userAgentID string) (*model.AuthRequest, error) {
request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id)
if err != nil { if err != nil {
@ -545,27 +591,19 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR
return nil, err return nil, err
} }
if (request.SelectedIDPConfigID != "" || userSession.SelectedIDPConfigID != "") && (request.LinkingUsers == nil || len(request.LinkingUsers) == 0) { isInternalLogin := request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == ""
if !checkVerificationTime(userSession.ExternalLoginVerification, repo.ExternalLoginCheckLifeTime) { if !isInternalLogin && len(request.LinkingUsers) == 0 && !checkVerificationTime(userSession.ExternalLoginVerification, repo.ExternalLoginCheckLifeTime) {
selectedIDPConfigID := request.SelectedIDPConfigID selectedIDPConfigID := request.SelectedIDPConfigID
if selectedIDPConfigID == "" { if selectedIDPConfigID == "" {
selectedIDPConfigID = userSession.SelectedIDPConfigID selectedIDPConfigID = userSession.SelectedIDPConfigID
}
return append(steps, &model.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID}), nil
} }
} else if (request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == "") || (request.SelectedIDPConfigID != "" && request.LinkingUsers != nil && len(request.LinkingUsers) > 0) { return append(steps, &model.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID}), nil
if user.InitRequired { }
return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), 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) step, ok, err := repo.mfaChecked(userSession, request, user)
@ -624,21 +662,46 @@ func (repo *AuthRequestRepo) usersForUserSelection(request *model.AuthRequest) (
return users, nil 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) { func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *model.AuthRequest, user *user_model.UserView) (model.NextStep, bool, error) {
mfaLevel := request.MfaLevel() mfaLevel := request.MFALevel()
allowedProviders, required := user.MfaTypesAllowed(mfaLevel, request.LoginPolicy) allowedProviders, required := user.MFATypesAllowed(mfaLevel, request.LoginPolicy)
promptRequired := (user.MfaMaxSetUp < mfaLevel) || (len(allowedProviders) == 0 && required) promptRequired := (user.MFAMaxSetUp < mfaLevel) || (len(allowedProviders) == 0 && required)
if promptRequired || !repo.mfaSkippedOrSetUp(user) { if promptRequired || !repo.mfaSkippedOrSetUp(user) {
types := user.MfaTypesSetupPossible(mfaLevel, request.LoginPolicy) types := user.MFATypesSetupPossible(mfaLevel, request.LoginPolicy)
if promptRequired && len(types) == 0 { if promptRequired && len(types) == 0 {
return nil, false, errors.ThrowPreconditionFailed(nil, "LOGIN-5Hm8s", "Errors.Login.LoginPolicy.MFA.ForceAndNotConfigured") return nil, false, errors.ThrowPreconditionFailed(nil, "LOGIN-5Hm8s", "Errors.Login.LoginPolicy.MFA.ForceAndNotConfigured")
} }
if len(types) == 0 { if len(types) == 0 {
return nil, true, nil return nil, true, nil
} }
return &model.MfaPromptStep{ return &model.MFAPromptStep{
Required: promptRequired, Required: promptRequired,
MfaProviders: types, MFAProviders: types,
}, false, nil }, false, nil
} }
switch mfaLevel { switch mfaLevel {
@ -651,28 +714,28 @@ func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView,
fallthrough fallthrough
case model.MFALevelSecondFactor: case model.MFALevelSecondFactor:
if checkVerificationTime(userSession.SecondFactorVerification, repo.SecondFactorCheckLifeTime) { if checkVerificationTime(userSession.SecondFactorVerification, repo.SecondFactorCheckLifeTime) {
request.MfasVerified = append(request.MfasVerified, userSession.SecondFactorVerificationType) request.MFAsVerified = append(request.MFAsVerified, userSession.SecondFactorVerificationType)
request.AuthTime = userSession.SecondFactorVerification request.AuthTime = userSession.SecondFactorVerification
return nil, true, nil return nil, true, nil
} }
fallthrough fallthrough
case model.MFALevelMultiFactor: case model.MFALevelMultiFactor:
if checkVerificationTime(userSession.MultiFactorVerification, repo.MultiFactorCheckLifeTime) { if checkVerificationTime(userSession.MultiFactorVerification, repo.MultiFactorCheckLifeTime) {
request.MfasVerified = append(request.MfasVerified, userSession.MultiFactorVerificationType) request.MFAsVerified = append(request.MFAsVerified, userSession.MultiFactorVerificationType)
request.AuthTime = userSession.MultiFactorVerification request.AuthTime = userSession.MultiFactorVerification
return nil, true, nil return nil, true, nil
} }
} }
return &model.MfaVerificationStep{ return &model.MFAVerificationStep{
MfaProviders: allowedProviders, MFAProviders: allowedProviders,
}, false, nil }, false, nil
} }
func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool { func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool {
if user.MfaMaxSetUp > model.MFALevelNotSetUp { if user.MFAMaxSetUp > model.MFALevelNotSetUp {
return true 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) { 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.HumanExternalLoginCheckSucceeded,
es_model.HumanMFAOTPCheckSucceeded, es_model.HumanMFAOTPCheckSucceeded,
es_model.HumanMFAOTPCheckFailed, 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) eventData, err := user_view_model.UserSessionFromEvent(event)
if err != nil { if err != nil {
logging.Log("EVENT-sdgT3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error getting event data") logging.Log("EVENT-sdgT3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error getting event data")

View File

@ -48,8 +48,10 @@ func (m *mockViewErrUserSession) UserSessionsByAgentID(string) ([]*user_view_mod
type mockViewUserSession struct { type mockViewUserSession struct {
ExternalLoginVerification time.Time ExternalLoginVerification time.Time
PasswordlessVerification time.Time
PasswordVerification time.Time PasswordVerification time.Time
SecondFactorVerification time.Time SecondFactorVerification time.Time
MultiFactorVerification time.Time
Users []mockUser Users []mockUser
} }
@ -61,8 +63,10 @@ type mockUser struct {
func (m *mockViewUserSession) UserSessionByIDs(string, string) (*user_view_model.UserSessionView, error) { func (m *mockViewUserSession) UserSessionByIDs(string, string) (*user_view_model.UserSessionView, error) {
return &user_view_model.UserSessionView{ return &user_view_model.UserSessionView{
ExternalLoginVerification: m.ExternalLoginVerification, ExternalLoginVerification: m.ExternalLoginVerification,
PasswordlessVerification: m.PasswordlessVerification,
PasswordVerification: m.PasswordVerification, PasswordVerification: m.PasswordVerification,
SecondFactorVerification: m.SecondFactorVerification, SecondFactorVerification: m.SecondFactorVerification,
MultiFactorVerification: m.MultiFactorVerification,
}, nil }, nil
} }
@ -115,8 +119,9 @@ type mockViewUser struct {
PasswordChangeRequired bool PasswordChangeRequired bool
IsEmailVerified bool IsEmailVerified bool
OTPState int32 OTPState int32
MfaMaxSetUp int32 MFAMaxSetUp int32
MfaInitSkipped time.Time MFAInitSkipped time.Time
PasswordlessTokens user_view_model.WebAuthNTokens
} }
type mockLoginPolicy struct { type mockLoginPolicy struct {
@ -138,8 +143,9 @@ func (m *mockViewUser) UserByID(string) (*user_view_model.UserView, error) {
PasswordChangeRequired: m.PasswordChangeRequired, PasswordChangeRequired: m.PasswordChangeRequired,
IsEmailVerified: m.IsEmailVerified, IsEmailVerified: m.IsEmailVerified,
OTPState: m.OTPState, OTPState: m.OTPState,
MfaMaxSetUp: m.MfaMaxSetUp, MFAMaxSetUp: m.MFAMaxSetUp,
MfaInitSkipped: m.MfaInitSkipped, MFAInitSkipped: m.MFAInitSkipped,
PasswordlessTokens: m.PasswordlessTokens,
}, },
}, nil }, nil
} }
@ -200,7 +206,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
loginPolicyProvider loginPolicyViewProvider loginPolicyProvider loginPolicyViewProvider
PasswordCheckLifeTime time.Duration PasswordCheckLifeTime time.Duration
ExternalLoginCheckLifeTime time.Duration ExternalLoginCheckLifeTime time.Duration
MfaInitSkippedLifeTime time.Duration MFAInitSkippedLifeTime time.Duration
SecondFactorCheckLifeTime time.Duration SecondFactorCheckLifeTime time.Duration
MultiFactorCheckLifeTime time.Duration MultiFactorCheckLifeTime time.Duration
} }
@ -413,6 +419,49 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}}, }},
nil, 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", "password not set, init password step",
fields{ fields{
@ -433,7 +482,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}, },
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -452,7 +501,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}, },
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -499,7 +548,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -525,8 +574,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}, },
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
OTPState: int32(user_model.MfaStateReady), OTPState: int32(user_model.MFAStateReady),
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -540,8 +589,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
SecondFactors: []iam_model.SecondFactorType{iam_model.SecondFactorTypeOTP}, SecondFactors: []iam_model.SecondFactorType{iam_model.SecondFactorTypeOTP},
}, },
}, false}, }, false},
[]model.NextStep{&model.MfaVerificationStep{ []model.NextStep{&model.MFAVerificationStep{
MfaProviders: []model.MFAType{model.MFATypeOTP}, MFAProviders: []model.MFAType{model.MFATypeOTP},
}}, }},
nil, nil,
}, },
@ -554,8 +603,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}, },
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
OTPState: int32(user_model.MfaStateReady), OTPState: int32(user_model.MFAStateReady),
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -571,8 +620,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
SecondFactors: []iam_model.SecondFactorType{iam_model.SecondFactorTypeOTP}, SecondFactors: []iam_model.SecondFactorType{iam_model.SecondFactorTypeOTP},
}, },
}, false}, }, false},
[]model.NextStep{&model.MfaVerificationStep{ []model.NextStep{&model.MFAVerificationStep{
MfaProviders: []model.MFAType{model.MFATypeOTP}, MFAProviders: []model.MFAType{model.MFATypeOTP},
}}, }},
nil, nil,
}, },
@ -587,7 +636,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
PasswordSet: true, PasswordSet: true,
PasswordChangeRequired: true, PasswordChangeRequired: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -613,7 +662,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}, },
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -639,7 +688,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
PasswordChangeRequired: true, PasswordChangeRequired: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -665,7 +714,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -693,7 +742,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -722,7 +771,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -754,7 +803,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -785,7 +834,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -810,7 +859,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MFALevelSecondFactor), MFAMaxSetUp: int32(model.MFALevelSecondFactor),
}, },
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
@ -844,7 +893,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
LoginPolicyViewProvider: tt.fields.loginPolicyProvider, LoginPolicyViewProvider: tt.fields.loginPolicyProvider,
PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime, PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime,
ExternalLoginCheckLifeTime: tt.fields.ExternalLoginCheckLifeTime, ExternalLoginCheckLifeTime: tt.fields.ExternalLoginCheckLifeTime,
MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, MFAInitSkippedLifeTime: tt.fields.MFAInitSkippedLifeTime,
SecondFactorCheckLifeTime: tt.fields.SecondFactorCheckLifeTime, SecondFactorCheckLifeTime: tt.fields.SecondFactorCheckLifeTime,
MultiFactorCheckLifeTime: tt.fields.MultiFactorCheckLifeTime, MultiFactorCheckLifeTime: tt.fields.MultiFactorCheckLifeTime,
} }
@ -860,7 +909,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
func TestAuthRequestRepo_mfaChecked(t *testing.T) { func TestAuthRequestRepo_mfaChecked(t *testing.T) {
type fields struct { type fields struct {
MfaInitSkippedLifeTime time.Duration MFAInitSkippedLifeTime time.Duration
SecondFactorCheckLifeTime time.Duration SecondFactorCheckLifeTime time.Duration
MultiFactorCheckLifeTime time.Duration MultiFactorCheckLifeTime time.Duration
} }
@ -884,7 +933,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
// args{ // args{
// request: &model.AuthRequest{PossibleLOAs: []model.LevelOfAssurance{}}, // request: &model.AuthRequest{PossibleLOAs: []model.LevelOfAssurance{}},
// user: &user_model.UserView{ // user: &user_model.UserView{
// OTPState: user_model.MfaStateReady, // OTPState: user_model.MFAStateReady,
// }, // },
// }, // },
// false, // false,
@ -892,7 +941,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
{ {
"not set up, forced by policy, no mfas configured, error", "not set up, forced by policy, no mfas configured, error",
fields{ fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour, MFAInitSkippedLifeTime: 30 * 24 * time.Hour,
}, },
args{ args{
request: &model.AuthRequest{ request: &model.AuthRequest{
@ -902,7 +951,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
}, },
user: &user_model.UserView{ user: &user_model.UserView{
HumanView: &user_model.HumanView{ 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", "not set up, no mfas configured, no prompt and true",
fields{ fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour, MFAInitSkippedLifeTime: 30 * 24 * time.Hour,
}, },
args{ args{
request: &model.AuthRequest{ request: &model.AuthRequest{
@ -921,7 +970,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
}, },
user: &user_model.UserView{ user: &user_model.UserView{
HumanView: &user_model.HumanView{ 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", "not set up, prompt and false",
fields{ fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour, MFAInitSkippedLifeTime: 30 * 24 * time.Hour,
}, },
args{ args{
request: &model.AuthRequest{ request: &model.AuthRequest{
@ -942,12 +991,12 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
}, },
user: &user_model.UserView{ user: &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: model.MFALevelNotSetUp, MFAMaxSetUp: model.MFALevelNotSetUp,
}, },
}, },
}, },
&model.MfaPromptStep{ &model.MFAPromptStep{
MfaProviders: []model.MFAType{ MFAProviders: []model.MFAType{
model.MFATypeOTP, model.MFATypeOTP,
}, },
}, },
@ -957,7 +1006,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
{ {
"not set up, forced by org, true", "not set up, forced by org, true",
fields{ fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour, MFAInitSkippedLifeTime: 30 * 24 * time.Hour,
}, },
args{ args{
request: &model.AuthRequest{ request: &model.AuthRequest{
@ -968,13 +1017,13 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
}, },
user: &user_model.UserView{ user: &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: model.MFALevelNotSetUp, MFAMaxSetUp: model.MFALevelNotSetUp,
}, },
}, },
}, },
&model.MfaPromptStep{ &model.MFAPromptStep{
Required: true, Required: true,
MfaProviders: []model.MFAType{ MFAProviders: []model.MFAType{
model.MFATypeOTP, model.MFATypeOTP,
}, },
}, },
@ -984,7 +1033,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
{ {
"not set up and skipped, true", "not set up and skipped, true",
fields{ fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour, MFAInitSkippedLifeTime: 30 * 24 * time.Hour,
}, },
args{ args{
request: &model.AuthRequest{ request: &model.AuthRequest{
@ -992,8 +1041,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
}, },
user: &user_model.UserView{ user: &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: model.MFALevelNotSetUp, MFAMaxSetUp: model.MFALevelNotSetUp,
MfaInitSkipped: time.Now().UTC(), MFAInitSkipped: time.Now().UTC(),
}, },
}, },
}, },
@ -1014,8 +1063,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
}, },
user: &user_model.UserView{ user: &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: model.MFALevelSecondFactor, MFAMaxSetUp: model.MFALevelSecondFactor,
OTPState: user_model.MfaStateReady, OTPState: user_model.MFAStateReady,
}, },
}, },
userSession: &user_model.UserSessionView{SecondFactorVerification: time.Now().UTC().Add(-5 * time.Hour)}, 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{ user: &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: model.MFALevelSecondFactor, MFAMaxSetUp: model.MFALevelSecondFactor,
OTPState: user_model.MfaStateReady, OTPState: user_model.MFAStateReady,
}, },
}, },
userSession: &user_model.UserSessionView{}, userSession: &user_model.UserSessionView{},
}, },
&model.MfaVerificationStep{ &model.MFAVerificationStep{
MfaProviders: []model.MFAType{model.MFATypeOTP}, MFAProviders: []model.MFAType{model.MFATypeOTP},
}, },
false, false,
nil, nil,
@ -1054,7 +1103,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
repo := &AuthRequestRepo{ repo := &AuthRequestRepo{
MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, MFAInitSkippedLifeTime: tt.fields.MFAInitSkippedLifeTime,
SecondFactorCheckLifeTime: tt.fields.SecondFactorCheckLifeTime, SecondFactorCheckLifeTime: tt.fields.SecondFactorCheckLifeTime,
MultiFactorCheckLifeTime: tt.fields.MultiFactorCheckLifeTime, MultiFactorCheckLifeTime: tt.fields.MultiFactorCheckLifeTime,
} }
@ -1073,7 +1122,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) { func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
type fields struct { type fields struct {
MfaInitSkippedLifeTime time.Duration MFAInitSkippedLifeTime time.Duration
} }
type args struct { type args struct {
user *user_model.UserView user *user_model.UserView
@ -1090,7 +1139,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
args{ args{
&user_model.UserView{ &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: model.MFALevelSecondFactor, MFAMaxSetUp: model.MFALevelSecondFactor,
}, },
}, },
}, },
@ -1099,13 +1148,13 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
{ {
"mfa skipped active, true", "mfa skipped active, true",
fields{ fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour, MFAInitSkippedLifeTime: 30 * 24 * time.Hour,
}, },
args{ args{
&user_model.UserView{ &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: -1, MFAMaxSetUp: -1,
MfaInitSkipped: time.Now().UTC().Add(-10 * time.Hour), MFAInitSkipped: time.Now().UTC().Add(-10 * time.Hour),
}, },
}, },
}, },
@ -1114,13 +1163,13 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
{ {
"mfa skipped inactive, false", "mfa skipped inactive, false",
fields{ fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour, MFAInitSkippedLifeTime: 30 * 24 * time.Hour,
}, },
args{ args{
&user_model.UserView{ &user_model.UserView{
HumanView: &user_model.HumanView{ HumanView: &user_model.HumanView{
MfaMaxSetUp: -1, MFAMaxSetUp: -1,
MfaInitSkipped: time.Now().UTC().Add(-40 * 24 * time.Hour), MFAInitSkipped: time.Now().UTC().Add(-40 * 24 * time.Hour),
}, },
}, },
}, },
@ -1130,7 +1179,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
repo := &AuthRequestRepo{ repo := &AuthRequestRepo{
MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, MFAInitSkippedLifeTime: tt.fields.MFAInitSkippedLifeTime,
} }
if got := repo.mfaSkippedOrSetUp(tt.args.user); got != tt.want { if got := repo.mfaSkippedOrSetUp(tt.args.user); got != tt.want {
t.Errorf("mfaSkippedOrSetUp() = %v, want %v", got, tt.want) t.Errorf("mfaSkippedOrSetUp() = %v, want %v", got, tt.want)

View File

@ -253,18 +253,22 @@ func (repo *UserRepo) ChangePassword(ctx context.Context, userID, old, new strin
return err 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) user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.OTPState == model.MfaStateUnspecified { mfas := make([]*model.MultiFactor, 0)
return []*model.MultiFactor{}, nil 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 := "" accountName := ""
user, err := repo.UserByID(ctx, userID) user, err := repo.UserByID(ctx, userID)
if err != nil { 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) 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 := "" accountName := ""
user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID)
if err != nil { 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) return repo.UserEvents.AddOTP(ctx, authz.GetCtxData(ctx).UserID, accountName)
} }
func (repo *UserRepo) VerifyMfaOTPSetup(ctx context.Context, userID, code string) error { func (repo *UserRepo) VerifyMFAOTPSetup(ctx context.Context, userID, code string) error {
return repo.UserEvents.CheckMfaOTPSetup(ctx, userID, code) return repo.UserEvents.CheckMFAOTPSetup(ctx, userID, code)
} }
func (repo *UserRepo) VerifyMyMfaOTPSetup(ctx context.Context, code string) error { func (repo *UserRepo) VerifyMyMFAOTPSetup(ctx context.Context, code string) error {
return repo.UserEvents.CheckMfaOTPSetup(ctx, authz.GetCtxData(ctx).UserID, code) 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) 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 { func (repo *UserRepo) ChangeMyUsername(ctx context.Context, username string) error {
ctxData := authz.GetCtxData(ctx) ctxData := authz.GetCtxData(ctx)
orgPolicy, err := repo.View.OrgIAMPolicyByAggregateID(ctxData.OrgID) 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) return repo.UserEvents.VerifyInitCode(ctx, pwPolicyView, userID, code, password)
} }
func (repo *UserRepo) SkipMfaInit(ctx context.Context, userID string) error { func (repo *UserRepo) SkipMFAInit(ctx context.Context, userID string) error {
return repo.UserEvents.SkipMfaInit(ctx, userID) return repo.UserEvents.SkipMFAInit(ctx, userID)
} }
func (repo *UserRepo) RequestPasswordReset(ctx context.Context, loginname string) error { func (repo *UserRepo) RequestPasswordReset(ctx context.Context, loginname string) error {

View File

@ -67,7 +67,7 @@ func (u *User) ProcessUser(event *models.Event) (err error) {
if err != nil { if err != nil {
return err return err
} }
u.fillLoginNames(user) err = u.fillLoginNames(user)
case es_model.UserProfileChanged, case es_model.UserProfileChanged,
es_model.UserEmailChanged, es_model.UserEmailChanged,
es_model.UserEmailVerified, es_model.UserEmailVerified,
@ -94,6 +94,12 @@ func (u *User) ProcessUser(event *models.Event) (err error) {
es_model.HumanMFAOTPAdded, es_model.HumanMFAOTPAdded,
es_model.HumanMFAOTPVerified, es_model.HumanMFAOTPVerified,
es_model.HumanMFAOTPRemoved, 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.HumanMFAInitSkipped,
es_model.MachineChanged, es_model.MachineChanged,
es_model.HumanPasswordChanged: es_model.HumanPasswordChanged:

View File

@ -48,6 +48,10 @@ func (u *UserSession) Reduce(event *models.Event) (err error) {
es_model.HumanExternalLoginCheckSucceeded, es_model.HumanExternalLoginCheckSucceeded,
es_model.HumanMFAOTPCheckSucceeded, es_model.HumanMFAOTPCheckSucceeded,
es_model.HumanMFAOTPCheckFailed, es_model.HumanMFAOTPCheckFailed,
es_model.HumanMFAU2FTokenCheckSucceeded,
es_model.HumanMFAU2FTokenCheckFailed,
es_model.HumanPasswordlessTokenCheckSucceeded,
es_model.HumanPasswordlessTokenCheckFailed,
es_model.HumanSignedOut: es_model.HumanSignedOut:
eventData, err := view_model.UserSessionFromEvent(event) eventData, err := view_model.UserSessionFromEvent(event)
if err != nil { if err != nil {
@ -78,7 +82,9 @@ func (u *UserSession) Reduce(event *models.Event) (err error) {
es_model.DomainClaimed, es_model.DomainClaimed,
es_model.UserUserNameChanged, es_model.UserUserNameChanged,
es_model.HumanExternalIDPRemoved, es_model.HumanExternalIDPRemoved,
es_model.HumanExternalIDPCascadeRemoved: es_model.HumanExternalIDPCascadeRemoved,
es_model.HumanPasswordlessTokenRemoved,
es_model.HumanMFAU2FTokenRemoved:
sessions, err := u.view.UserSessionsByUserID(event.AggregateID) sessions, err := u.view.UserSessionsByUserID(event.AggregateID)
if err != nil { if err != nil {
return err return err

View File

@ -138,7 +138,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
IdGenerator: idGenerator, IdGenerator: idGenerator,
PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration, MFAInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MFAInitSkip.Duration,
SecondFactorCheckLifeTime: systemDefaults.VerificationLifetimes.SecondFactorCheck.Duration, SecondFactorCheckLifeTime: systemDefaults.VerificationLifetimes.SecondFactorCheck.Duration,
MultiFactorCheckLifeTime: systemDefaults.VerificationLifetimes.MultiFactorCheck.Duration, MultiFactorCheckLifeTime: systemDefaults.VerificationLifetimes.MultiFactorCheck.Duration,
IAMID: systemDefaults.IamID, IAMID: systemDefaults.IamID,

View File

@ -48,8 +48,8 @@ func (v *View) IsUserUnique(userName, email string) (bool, error) {
return view.IsUserUnique(v.Db, userTable, userName, email) return view.IsUserUnique(v.Db, userTable, userName, email)
} }
func (v *View) UserMfas(userID string) ([]*usr_model.MultiFactor, error) { func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) {
return view.UserMfas(v.Db, userTable, userID) return view.UserMFAs(v.Db, userTable, userID)
} }
func (v *View) PutUser(user *model.UserView, sequence uint64, eventTimestamp time.Time) error { func (v *View) PutUser(user *model.UserView, sequence uint64, eventTimestamp time.Time) error {

View File

@ -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) RegisterExternalUser(ctx context.Context, user *model.User, externalIDP *model.ExternalIDP, member *org_model.OrgMember, resourceOwner string) (*model.User, error)
myUserRepo myUserRepo
SkipMfaInit(ctx context.Context, userID string) error SkipMFAInit(ctx context.Context, userID string) error
RequestPasswordReset(ctx context.Context, username string) error RequestPasswordReset(ctx context.Context, username string) error
SetPassword(ctx context.Context, userID, code, password 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 VerifyInitCode(ctx context.Context, userID, code, password string) error
ResendInitVerificationMail(ctx context.Context, userID string) error ResendInitVerificationMail(ctx context.Context, userID string) error
AddMfaOTP(ctx context.Context, userID string) (*model.OTP, error) AddMFAOTP(ctx context.Context, userID string) (*model.OTP, error)
VerifyMfaOTPSetup(ctx context.Context, userID, code string) 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 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) AddMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) (*model.ExternalIDP, error)
RemoveMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error RemoveMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error
MyUserMfas(ctx context.Context) ([]*model.MultiFactor, error) MyUserMFAs(ctx context.Context) ([]*model.MultiFactor, error)
AddMyMfaOTP(ctx context.Context) (*model.OTP, error) AddMyMFAOTP(ctx context.Context) (*model.OTP, error)
VerifyMyMfaOTPSetup(ctx context.Context, code string) error VerifyMyMFAOTPSetup(ctx context.Context, code string) error
RemoveMyMfaOTP(ctx context.Context) 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 ChangeMyUsername(ctx context.Context, username string) error

View File

@ -34,7 +34,7 @@ type AuthRequest struct {
LinkingUsers []*ExternalUser LinkingUsers []*ExternalUser
PossibleSteps []NextStep PossibleSteps []NextStep
PasswordVerified bool PasswordVerified bool
MfasVerified []MFAType MFAsVerified []MFAType
Audience []string Audience []string
AuthTime time.Time AuthTime time.Time
Code string Code string
@ -109,7 +109,7 @@ func (a *AuthRequest) IsValid() bool {
a.Request != nil && a.Request.IsValid() a.Request != nil && a.Request.IsValid()
} }
func (a *AuthRequest) MfaLevel() MFALevel { func (a *AuthRequest) MFALevel() MFALevel {
return -1 return -1
//PLANNED: check a.PossibleLOAs (and Prompt Login?) //PLANNED: check a.PossibleLOAs (and Prompt Login?)
} }

View File

@ -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 { type fields struct {
Prompt Prompt Prompt Prompt
PossibleLOAs []LevelOfAssurance PossibleLOAs []LevelOfAssurance
@ -169,7 +169,7 @@ func TestAuthRequest_MfaLevel(t *testing.T) {
Prompt: tt.fields.Prompt, Prompt: tt.fields.Prompt,
PossibleLOAs: tt.fields.PossibleLOAs, 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) t.Errorf("MFALevel() = %v, want %v", got, tt.want)
} }
}) })

View File

@ -15,14 +15,15 @@ const (
NextStepChangePassword NextStepChangePassword
NextStepInitPassword NextStepInitPassword
NextStepVerifyEmail NextStepVerifyEmail
NextStepMfaPrompt NextStepMFAPrompt
NextStepMfaVerify NextStepMFAVerify
NextStepRedirectToCallback NextStepRedirectToCallback
NextStepChangeUsername NextStepChangeUsername
NextStepLinkUsers NextStepLinkUsers
NextStepExternalNotFoundOption NextStepExternalNotFoundOption
NextStepExternalLogin NextStepExternalLogin
NextStepGrantRequired NextStepGrantRequired
NextStepPasswordless
) )
type UserSessionState int32 type UserSessionState int32
@ -81,6 +82,12 @@ func (s *ExternalLoginStep) Type() NextStepType {
return NextStepExternalLogin return NextStepExternalLogin
} }
type PasswordlessStep struct{}
func (s *PasswordlessStep) Type() NextStepType {
return NextStepPasswordless
}
type ChangePasswordStep struct{} type ChangePasswordStep struct{}
func (s *ChangePasswordStep) Type() NextStepType { func (s *ChangePasswordStep) Type() NextStepType {
@ -105,21 +112,21 @@ func (s *VerifyEMailStep) Type() NextStepType {
return NextStepVerifyEmail return NextStepVerifyEmail
} }
type MfaPromptStep struct { type MFAPromptStep struct {
Required bool Required bool
MfaProviders []MFAType MFAProviders []MFAType
} }
func (s *MfaPromptStep) Type() NextStepType { func (s *MFAPromptStep) Type() NextStepType {
return NextStepMfaPrompt return NextStepMFAPrompt
} }
type MfaVerificationStep struct { type MFAVerificationStep struct {
MfaProviders []MFAType MFAProviders []MFAType
} }
func (s *MfaVerificationStep) Type() NextStepType { func (s *MFAVerificationStep) Type() NextStepType {
return NextStepMfaVerify return NextStepMFAVerify
} }
type LinkUsersStep struct{} type LinkUsersStep struct{}
@ -145,6 +152,7 @@ type MFAType int
const ( const (
MFATypeOTP MFAType = iota MFATypeOTP MFAType = iota
MFATypeU2F MFATypeU2F
MFATypeU2FUserVerification
) )
type MFALevel int type MFALevel int

View File

@ -23,6 +23,7 @@ type SystemDefaults struct {
DomainVerification DomainVerification DomainVerification DomainVerification
IamID string IamID string
Notifications Notifications Notifications Notifications
WebAuthN WebAuthN
} }
type ZitadelDocs struct { type ZitadelDocs struct {
@ -52,7 +53,7 @@ type OTPConfig struct {
type VerificationLifetimes struct { type VerificationLifetimes struct {
PasswordCheck types.Duration PasswordCheck types.Duration
ExternalLoginCheck types.Duration ExternalLoginCheck types.Duration
MfaInitSkip types.Duration MFAInitSkip types.Duration
SecondFactorCheck types.Duration SecondFactorCheck types.Duration
MultiFactorCheck types.Duration MultiFactorCheck types.Duration
} }
@ -89,3 +90,9 @@ type TemplateData struct {
VerifyPhone templates.TemplateData VerifyPhone templates.TemplateData
DomainClaimed templates.TemplateData DomainClaimed templates.TemplateData
} }
type WebAuthN struct {
ID string
Origin string
DisplayName string
}

View File

@ -14,6 +14,7 @@ const (
Step5 Step5
Step6 Step6
Step7 Step7
Step8
//StepCount marks the the length of possible steps (StepCount-1 == last possible step) //StepCount marks the the length of possible steps (StepCount-1 == last possible step)
StepCount StepCount
) )

View File

@ -16,6 +16,7 @@ type LoginPolicy struct {
ForceMFA bool ForceMFA bool
SecondFactors []SecondFactorType SecondFactors []SecondFactorType
MultiFactors []MultiFactorType MultiFactors []MultiFactorType
PasswordlessType PasswordlessType
} }
type IDPProvider struct { type IDPProvider struct {
@ -53,6 +54,13 @@ const (
MultiFactorTypeU2FWithPIN MultiFactorTypeU2FWithPIN
) )
type PasswordlessType int32
const (
PasswordlessTypeNotAllowed PasswordlessType = iota
PasswordlessTypeAllowed
)
func (p *LoginPolicy) IsValid() bool { func (p *LoginPolicy) IsValid() bool {
return p.ObjectRoot.AggregateID != "" return p.ObjectRoot.AggregateID != ""
} }

View File

@ -11,6 +11,7 @@ type LoginPolicyView struct {
AllowRegister bool AllowRegister bool
AllowExternalIDP bool AllowExternalIDP bool
ForceMFA bool ForceMFA bool
PasswordlessType PasswordlessType
SecondFactors []SecondFactorType SecondFactors []SecondFactorType
MultiFactors []MultiFactorType MultiFactors []MultiFactorType
Default bool Default bool

View File

@ -127,8 +127,8 @@ func GetMockManipulateIAMWithLoginPolicy(ctrl *gomock.Controller) *IAMEventstore
func GetMockManipulateIAMWithLoginPolicyWithMFAs(ctrl *gomock.Controller) *IAMEventstore { func GetMockManipulateIAMWithLoginPolicyWithMFAs(ctrl *gomock.Controller) *IAMEventstore {
policyData, _ := json.Marshal(model.LoginPolicy{AllowRegister: true, AllowUsernamePassword: true, AllowExternalIdp: true}) policyData, _ := json.Marshal(model.LoginPolicy{AllowRegister: true, AllowUsernamePassword: true, AllowExternalIdp: true})
idpProviderData, _ := json.Marshal(model.IDPProvider{IDPConfigID: "IDPConfigID", Type: 1}) idpProviderData, _ := json.Marshal(model.IDPProvider{IDPConfigID: "IDPConfigID", Type: 1})
secondFactor, _ := json.Marshal(model.MFA{MfaType: int32(model2.SecondFactorTypeOTP)}) secondFactor, _ := json.Marshal(model.MFA{MFAType: int32(model2.SecondFactorTypeOTP)})
multiFactor, _ := json.Marshal(model.MFA{MfaType: int32(model2.MultiFactorTypeU2FWithPIN)}) multiFactor, _ := json.Marshal(model.MFA{MFAType: int32(model2.MultiFactorTypeU2FWithPIN)})
events := []*es_models.Event{ events := []*es_models.Event{
{AggregateID: "AggregateID", Sequence: 1, Type: model.IAMSetupStarted}, {AggregateID: "AggregateID", Sequence: 1, Type: model.IAMSetupStarted},
{AggregateID: "AggregateID", Sequence: 1, Type: model.LoginPolicyAdded, Data: policyData}, {AggregateID: "AggregateID", Sequence: 1, Type: model.LoginPolicyAdded, Data: policyData},

View File

@ -359,7 +359,7 @@ func LoginPolicySecondFactorAddedAggregate(aggCreator *es_models.AggregateCreato
AggregateTypeFilter(model.IAMAggregate). AggregateTypeFilter(model.IAMAggregate).
AggregateIDFilter(existing.AggregateID) AggregateIDFilter(existing.AggregateID)
validation := checkExistingLoginPolicySecondFactorValidation(mfa.MfaType) validation := checkExistingLoginPolicySecondFactorValidation(mfa.MFAType)
agg.SetPrecondition(validationQuery, validation) agg.SetPrecondition(validationQuery, validation)
return agg.AppendEvent(model.LoginPolicySecondFactorAdded, mfa) return agg.AppendEvent(model.LoginPolicySecondFactorAdded, mfa)
} }
@ -391,7 +391,7 @@ func LoginPolicyMultiFactorAddedAggregate(aggCreator *es_models.AggregateCreator
AggregateTypeFilter(model.IAMAggregate). AggregateTypeFilter(model.IAMAggregate).
AggregateIDFilter(existing.AggregateID) AggregateIDFilter(existing.AggregateID)
validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MfaType) validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MFAType)
agg.SetPrecondition(validationQuery, validation) agg.SetPrecondition(validationQuery, validation)
return agg.AppendEvent(model.LoginPolicyMultiFactorAdded, mfa) return agg.AppendEvent(model.LoginPolicyMultiFactorAdded, mfa)
} }
@ -689,7 +689,7 @@ func checkExistingLoginPolicySecondFactorValidation(mfaType int32) func(...*es_m
if err != nil { if err != nil {
return err return err
} }
mfas = append(mfas, idp.MfaType) mfas = append(mfas, idp.MFAType)
case model.LoginPolicySecondFactorRemoved: case model.LoginPolicySecondFactorRemoved:
mfa := new(model.MFA) mfa := new(model.MFA)
err := mfa.SetData(event) err := mfa.SetData(event)
@ -697,7 +697,7 @@ func checkExistingLoginPolicySecondFactorValidation(mfaType int32) func(...*es_m
return err return err
} }
for i := len(mfas) - 1; i >= 0; i-- { 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[i] = mfas[len(mfas)-1]
mfas[len(mfas)-1] = 0 mfas[len(mfas)-1] = 0
mfas = mfas[:len(mfas)-1] mfas = mfas[:len(mfas)-1]
@ -726,7 +726,7 @@ func checkExistingLoginPolicyMultiFactorValidation(mfaType int32) func(...*es_mo
if err != nil { if err != nil {
return err return err
} }
mfas = append(mfas, idp.MfaType) mfas = append(mfas, idp.MFAType)
case model.LoginPolicyMultiFactorRemoved: case model.LoginPolicyMultiFactorRemoved:
mfa := new(model.MFA) mfa := new(model.MFA)
err := mfa.SetData(event) err := mfa.SetData(event)
@ -734,7 +734,7 @@ func checkExistingLoginPolicyMultiFactorValidation(mfaType int32) func(...*es_mo
return err return err
} }
for i := len(mfas) - 1; i >= 0; i-- { 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[i] = mfas[len(mfas)-1]
mfas[len(mfas)-1] = 0 mfas[len(mfas)-1] = 0
mfas = mfas[:len(mfas)-1] mfas = mfas[:len(mfas)-1]

View File

@ -1497,7 +1497,7 @@ func TestLoginPolicySecondFactorAddedAggregate(t *testing.T) {
AllowUsernamePassword: true, AllowUsernamePassword: true,
}}, }},
newMFA: &model.MFA{ newMFA: &model.MFA{
MfaType: int32(iam_model.SecondFactorTypeOTP), MFAType: int32(iam_model.SecondFactorTypeOTP),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },
@ -1587,7 +1587,7 @@ func TestLoginPolicySecondFactorRemovedAggregate(t *testing.T) {
}, },
}}, }},
mfa: &model.MFA{ mfa: &model.MFA{
MfaType: int32(iam_model.SecondFactorTypeOTP), MFAType: int32(iam_model.SecondFactorTypeOTP),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },
@ -1674,7 +1674,7 @@ func TestLoginPolicyMultiFactorAddedAggregate(t *testing.T) {
AllowUsernamePassword: true, AllowUsernamePassword: true,
}}, }},
newMFA: &model.MFA{ newMFA: &model.MFA{
MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN), MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },
@ -1764,7 +1764,7 @@ func TestLoginPolicyMultiFactorRemovedAggregate(t *testing.T) {
}, },
}}, }},
mfa: &model.MFA{ mfa: &model.MFA{
MfaType: int32(iam_model.SecondFactorTypeOTP), MFAType: int32(iam_model.SecondFactorTypeOTP),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },

View File

@ -14,7 +14,8 @@ type LoginPolicy struct {
AllowUsernamePassword bool `json:"allowUsernamePassword"` AllowUsernamePassword bool `json:"allowUsernamePassword"`
AllowRegister bool `json:"allowRegister"` AllowRegister bool `json:"allowRegister"`
AllowExternalIdp bool `json:"allowExternalIdp"` AllowExternalIdp bool `json:"allowExternalIdp"`
ForceMFA bool `json:"forceMfa"` ForceMFA bool `json:"forceMFA"`
PasswordlessType int32 `json:"passwordlessType"`
IDPProviders []*IDPProvider `json:"-"` IDPProviders []*IDPProvider `json:"-"`
SecondFactors []int32 `json:"-"` SecondFactors []int32 `json:"-"`
MultiFactors []int32 `json:"-"` MultiFactors []int32 `json:"-"`
@ -31,7 +32,7 @@ type IDPProviderID struct {
} }
type MFA struct { type MFA struct {
MfaType int32 `json:"mfaType"` MFAType int32 `json:"mfaType"`
} }
func GetIDPProvider(providers []*IDPProvider, id string) (int, *IDPProvider) { func GetIDPProvider(providers []*IDPProvider, id string) (int, *IDPProvider) {
@ -65,6 +66,7 @@ func LoginPolicyToModel(policy *LoginPolicy) *iam_model.LoginPolicy {
ForceMFA: policy.ForceMFA, ForceMFA: policy.ForceMFA,
SecondFactors: secondFactors, SecondFactors: secondFactors,
MultiFactors: multiFactors, MultiFactors: multiFactors,
PasswordlessType: iam_model.PasswordlessType(policy.PasswordlessType),
} }
} }
@ -82,6 +84,7 @@ func LoginPolicyFromModel(policy *iam_model.LoginPolicy) *LoginPolicy {
ForceMFA: policy.ForceMFA, ForceMFA: policy.ForceMFA,
SecondFactors: secondFactors, SecondFactors: secondFactors,
MultiFactors: multiFactors, MultiFactors: multiFactors,
PasswordlessType: int32(policy.PasswordlessType),
} }
} }
@ -126,7 +129,7 @@ func SecondFactorsFromModel(mfas []iam_model.SecondFactorType) []int32 {
} }
func SecondFactorFromModel(mfa iam_model.SecondFactorType) *MFA { func SecondFactorFromModel(mfa iam_model.SecondFactorType) *MFA {
return &MFA{MfaType: int32(mfa)} return &MFA{MFAType: int32(mfa)}
} }
func SecondFactorsToModel(mfas []int32) []iam_model.SecondFactorType { 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 { func MultiFactorFromModel(mfa iam_model.MultiFactorType) *MFA {
return &MFA{MfaType: int32(mfa)} return &MFA{MFAType: int32(mfa)}
} }
func MultiFactorsToModel(mfas []int32) []iam_model.MultiFactorType { 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 { if changed.ForceMFA != p.ForceMFA {
changes["forceMFA"] = changed.ForceMFA changes["forceMFA"] = changed.ForceMFA
} }
if changed.PasswordlessType != p.PasswordlessType {
changes["passwordlessType"] = changed.PasswordlessType
}
return changes return changes
} }
@ -221,7 +227,7 @@ func (iam *IAM) appendAddSecondFactorToLoginPolicyEvent(event *es_models.Event)
if err != nil { if err != nil {
return err return err
} }
iam.DefaultLoginPolicy.SecondFactors = append(iam.DefaultLoginPolicy.SecondFactors, mfa.MfaType) iam.DefaultLoginPolicy.SecondFactors = append(iam.DefaultLoginPolicy.SecondFactors, mfa.MFAType)
return nil return nil
} }
@ -231,7 +237,7 @@ func (iam *IAM) appendRemoveSecondFactorFromLoginPolicyEvent(event *es_models.Ev
if err != nil { if err != nil {
return err 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[i] = iam.DefaultLoginPolicy.SecondFactors[len(iam.DefaultLoginPolicy.SecondFactors)-1]
iam.DefaultLoginPolicy.SecondFactors[len(iam.DefaultLoginPolicy.SecondFactors)-1] = 0 iam.DefaultLoginPolicy.SecondFactors[len(iam.DefaultLoginPolicy.SecondFactors)-1] = 0
iam.DefaultLoginPolicy.SecondFactors = iam.DefaultLoginPolicy.SecondFactors[:len(iam.DefaultLoginPolicy.SecondFactors)-1] 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 { if err != nil {
return err return err
} }
iam.DefaultLoginPolicy.MultiFactors = append(iam.DefaultLoginPolicy.MultiFactors, mfa.MfaType) iam.DefaultLoginPolicy.MultiFactors = append(iam.DefaultLoginPolicy.MultiFactors, mfa.MFAType)
return nil return nil
} }
@ -256,7 +262,7 @@ func (iam *IAM) appendRemoveMultiFactorFromLoginPolicyEvent(event *es_models.Eve
if err != nil { if err != nil {
return err 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[i] = iam.DefaultLoginPolicy.MultiFactors[len(iam.DefaultLoginPolicy.MultiFactors)-1]
iam.DefaultLoginPolicy.MultiFactors[len(iam.DefaultLoginPolicy.MultiFactors)-1] = 0 iam.DefaultLoginPolicy.MultiFactors[len(iam.DefaultLoginPolicy.MultiFactors)-1] = 0
iam.DefaultLoginPolicy.MultiFactors = iam.DefaultLoginPolicy.MultiFactors[:len(iam.DefaultLoginPolicy.MultiFactors)-1] iam.DefaultLoginPolicy.MultiFactors = iam.DefaultLoginPolicy.MultiFactors[:len(iam.DefaultLoginPolicy.MultiFactors)-1]

View File

@ -275,7 +275,7 @@ func TestAppendAddSecondFactorToPolicyEvent(t *testing.T) {
name: "append add second factor to login policy event", name: "append add second factor to login policy event",
args: args{ args: args{
iam: &IAM{DefaultLoginPolicy: &LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, 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{}, event: &es_models.Event{},
}, },
result: &IAM{DefaultLoginPolicy: &LoginPolicy{ 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) { 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)) 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) 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{ SecondFactors: []int32{
int32(model.SecondFactorTypeOTP), int32(model.SecondFactorTypeOTP),
}}}, }}},
mfa: &MFA{MfaType: int32(model.SecondFactorTypeOTP)}, mfa: &MFA{MFAType: int32(model.SecondFactorTypeOTP)},
event: &es_models.Event{}, event: &es_models.Event{},
}, },
result: &IAM{DefaultLoginPolicy: &LoginPolicy{ result: &IAM{DefaultLoginPolicy: &LoginPolicy{
@ -359,7 +359,7 @@ func TestAppendAddMultiFactorToPolicyEvent(t *testing.T) {
name: "append add mfa to login policy event", name: "append add mfa to login policy event",
args: args{ args: args{
iam: &IAM{DefaultLoginPolicy: &LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, 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{}, event: &es_models.Event{},
}, },
result: &IAM{DefaultLoginPolicy: &LoginPolicy{ 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) { 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)) 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) 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{ MultiFactors: []int32{
int32(model.MultiFactorTypeU2FWithPIN), int32(model.MultiFactorTypeU2FWithPIN),
}}}, }}},
mfa: &MFA{MfaType: int32(model.MultiFactorTypeU2FWithPIN)}, mfa: &MFA{MFAType: int32(model.MultiFactorTypeU2FWithPIN)},
event: &es_models.Event{}, event: &es_models.Event{},
}, },
result: &IAM{DefaultLoginPolicy: &LoginPolicy{ result: &IAM{DefaultLoginPolicy: &LoginPolicy{

View File

@ -28,6 +28,7 @@ type LoginPolicyView struct {
AllowUsernamePassword bool `json:"allowUsernamePassword" gorm:"column:allow_username_password"` AllowUsernamePassword bool `json:"allowUsernamePassword" gorm:"column:allow_username_password"`
AllowExternalIDP bool `json:"allowExternalIdp" gorm:"column:allow_external_idp"` AllowExternalIDP bool `json:"allowExternalIdp" gorm:"column:allow_external_idp"`
ForceMFA bool `json:"forceMFA" gorm:"column:force_mfa"` ForceMFA bool `json:"forceMFA" gorm:"column:force_mfa"`
PasswordlessType int32 `json:"passwordlessType" gorm:"column:passwordless_type"`
SecondFactors pq.Int64Array `json:"-" gorm:"column:second_factors"` SecondFactors pq.Int64Array `json:"-" gorm:"column:second_factors"`
MultiFactors pq.Int64Array `json:"-" gorm:"column:multi_factors"` MultiFactors pq.Int64Array `json:"-" gorm:"column:multi_factors"`
Default bool `json:"-" gorm:"-"` Default bool `json:"-" gorm:"-"`
@ -45,6 +46,7 @@ func LoginPolicyViewFromModel(policy *model.LoginPolicyView) *LoginPolicyView {
AllowExternalIDP: policy.AllowExternalIDP, AllowExternalIDP: policy.AllowExternalIDP,
AllowUsernamePassword: policy.AllowUsernamePassword, AllowUsernamePassword: policy.AllowUsernamePassword,
ForceMFA: policy.ForceMFA, ForceMFA: policy.ForceMFA,
PasswordlessType: int32(policy.PasswordlessType),
SecondFactors: secondFactorsFromModel(policy.SecondFactors), SecondFactors: secondFactorsFromModel(policy.SecondFactors),
MultiFactors: multiFactorsFromModel(policy.MultiFactors), MultiFactors: multiFactorsFromModel(policy.MultiFactors),
Default: policy.Default, Default: policy.Default,
@ -77,6 +79,7 @@ func LoginPolicyViewToModel(policy *LoginPolicyView) *model.LoginPolicyView {
AllowExternalIDP: policy.AllowExternalIDP, AllowExternalIDP: policy.AllowExternalIDP,
AllowUsernamePassword: policy.AllowUsernamePassword, AllowUsernamePassword: policy.AllowUsernamePassword,
ForceMFA: policy.ForceMFA, ForceMFA: policy.ForceMFA,
PasswordlessType: model.PasswordlessType(policy.PasswordlessType),
SecondFactors: secondFactorsToModel(policy.SecondFactors), SecondFactors: secondFactorsToModel(policy.SecondFactors),
MultiFactors: multiFactorsToToModel(policy.MultiFactors), MultiFactors: multiFactorsToToModel(policy.MultiFactors),
Default: policy.Default, Default: policy.Default,
@ -115,7 +118,7 @@ func (p *LoginPolicyView) AppendEvent(event *models.Event) (err error) {
if err != nil { if err != nil {
return err 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: case es_model.LoginPolicySecondFactorRemoved, org_es_model.LoginPolicySecondFactorRemoved:
err = p.removeSecondFactor(event) err = p.removeSecondFactor(event)
case es_model.LoginPolicyMultiFactorAdded, org_es_model.LoginPolicyMultiFactorAdded: case es_model.LoginPolicyMultiFactorAdded, org_es_model.LoginPolicyMultiFactorAdded:
@ -124,7 +127,7 @@ func (p *LoginPolicyView) AppendEvent(event *models.Event) (err error) {
if err != nil { if err != nil {
return err 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: case es_model.LoginPolicyMultiFactorRemoved, org_es_model.LoginPolicyMultiFactorRemoved:
err = p.removeMultiFactor(event) err = p.removeMultiFactor(event)
} }
@ -150,7 +153,7 @@ func (p *LoginPolicyView) removeSecondFactor(event *models.Event) error {
return err return err
} }
for i := len(p.SecondFactors) - 1; i >= 0; i-- { 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:]) copy(p.SecondFactors[i:], p.SecondFactors[i+1:])
p.SecondFactors[len(p.SecondFactors)-1] = 0 p.SecondFactors[len(p.SecondFactors)-1] = 0
p.SecondFactors = p.SecondFactors[:len(p.SecondFactors)-1] p.SecondFactors = p.SecondFactors[:len(p.SecondFactors)-1]
@ -167,7 +170,7 @@ func (p *LoginPolicyView) removeMultiFactor(event *models.Event) error {
return err return err
} }
for i := len(p.MultiFactors) - 1; i >= 0; i-- { 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:]) copy(p.MultiFactors[i:], p.MultiFactors[i+1:])
p.MultiFactors[len(p.MultiFactors)-1] = 0 p.MultiFactors[len(p.MultiFactors)-1] = 0
p.MultiFactors = p.MultiFactors[:len(p.MultiFactors)-1] p.MultiFactors = p.MultiFactors[:len(p.MultiFactors)-1]

View File

@ -209,7 +209,7 @@ func (repo *UserRepo) IsUserUnique(ctx context.Context, userName, email string)
return repo.View.IsUserUnique(userName, email) 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) user, err := repo.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -217,10 +217,10 @@ func (repo *UserRepo) UserMfas(ctx context.Context, userID string) ([]*usr_model
if user.HumanView == nil { if user.HumanView == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-xx0hV", "Errors.User.NotHuman") 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{}, 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 { func (repo *UserRepo) RemoveOTP(ctx context.Context, userID string) error {

View File

@ -91,6 +91,12 @@ func (u *User) ProcessUser(event *models.Event) (err error) {
es_model.HumanMFAOTPAdded, es_model.HumanMFAOTPAdded,
es_model.HumanMFAOTPVerified, es_model.HumanMFAOTPVerified,
es_model.HumanMFAOTPRemoved, es_model.HumanMFAOTPRemoved,
es_model.HumanMFAU2FTokenAdded,
es_model.HumanMFAU2FTokenVerified,
es_model.HumanMFAU2FTokenRemoved,
es_model.HumanPasswordlessTokenAdded,
es_model.HumanPasswordlessTokenVerified,
es_model.HumanPasswordlessTokenRemoved,
es_model.MachineChanged: es_model.MachineChanged:
user, err = u.view.UserByID(event.AggregateID) user, err = u.view.UserByID(event.AggregateID)
if err != nil { if err != nil {

View File

@ -36,8 +36,8 @@ func (v *View) IsUserUnique(userName, email string) (bool, error) {
return view.IsUserUnique(v.Db, userTable, userName, email) return view.IsUserUnique(v.Db, userTable, userName, email)
} }
func (v *View) UserMfas(userID string) ([]*usr_model.MultiFactor, error) { func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) {
return view.UserMfas(v.Db, userTable, userID) return view.UserMFAs(v.Db, userTable, userID)
} }
func (v *View) PutUsers(user []*model.UserView, sequence uint64, eventTimestamp time.Time) error { func (v *View) PutUsers(user []*model.UserView, sequence uint64, eventTimestamp time.Time) error {

View File

@ -30,7 +30,7 @@ type UserRepository interface {
ProfileByID(ctx context.Context, userID string) (*model.Profile, error) ProfileByID(ctx context.Context, userID string) (*model.Profile, error)
ChangeProfile(ctx context.Context, profile *model.Profile) (*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 RemoveOTP(ctx context.Context, userID string) error
SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error)

View File

@ -905,7 +905,7 @@ func (es *OrgEventstore) AddSecondFactorToLoginPolicy(ctx context.Context, aggre
if err != nil { if err != nil {
return 0, err 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 iam_model.SecondFactorType(m), nil
} }
return 0, errors.ThrowInternal(nil, "EVENT-rM9so", "Errors.Internal") return 0, errors.ThrowInternal(nil, "EVENT-rM9so", "Errors.Internal")
@ -950,7 +950,7 @@ func (es *OrgEventstore) AddMultiFactorToLoginPolicy(ctx context.Context, aggreg
if err != nil { if err != nil {
return 0, err 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 iam_model.MultiFactorType(m), nil
} }
return 0, errors.ThrowInternal(nil, "EVENT-2fMo0", "Errors.Internal") return 0, errors.ThrowInternal(nil, "EVENT-2fMo0", "Errors.Internal")

View File

@ -109,8 +109,8 @@ func GetMockChangesOrgWithLoginPolicyWithMFA(ctrl *gomock.Controller) *OrgEvents
orgData, _ := json.Marshal(model.Org{Name: "MusterOrg"}) orgData, _ := json.Marshal(model.Org{Name: "MusterOrg"})
loginPolicy, _ := json.Marshal(iam_es_model.LoginPolicy{AllowRegister: true, AllowExternalIdp: true, AllowUsernamePassword: true}) 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)}) 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)}) secondFactor, _ := json.Marshal(iam_es_model.MFA{MFAType: int32(iam_model.SecondFactorTypeOTP)})
multiFactor, _ := json.Marshal(iam_es_model.MFA{MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN)}) multiFactor, _ := json.Marshal(iam_es_model.MFA{MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN)})
events := []*es_models.Event{ events := []*es_models.Event{
{AggregateID: "AggregateID", Sequence: 1, Type: model.OrgAdded, Data: orgData}, {AggregateID: "AggregateID", Sequence: 1, Type: model.OrgAdded, Data: orgData},
{AggregateID: "AggregateID", Sequence: 1, Type: model.LoginPolicyAdded, Data: loginPolicy}, {AggregateID: "AggregateID", Sequence: 1, Type: model.LoginPolicyAdded, Data: loginPolicy},

View File

@ -105,7 +105,7 @@ func LoginPolicySecondFactorAddedAggregate(aggCreator *es_models.AggregateCreato
AggregateTypeFilter(model.OrgAggregate). AggregateTypeFilter(model.OrgAggregate).
AggregateIDsFilter(org.AggregateID) AggregateIDsFilter(org.AggregateID)
validation := checkExistingLoginPolicySecondFactorValidation(mfa.MfaType) validation := checkExistingLoginPolicySecondFactorValidation(mfa.MFAType)
agg.SetPrecondition(validationQuery, validation) agg.SetPrecondition(validationQuery, validation)
return agg.AppendEvent(model.LoginPolicySecondFactorAdded, mfa) return agg.AppendEvent(model.LoginPolicySecondFactorAdded, mfa)
} }
@ -137,7 +137,7 @@ func LoginPolicyMultiFactorAddedAggregate(aggCreator *es_models.AggregateCreator
AggregateTypeFilter(model.OrgAggregate). AggregateTypeFilter(model.OrgAggregate).
AggregateIDsFilter(org.AggregateID) AggregateIDsFilter(org.AggregateID)
validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MfaType) validation := checkExistingLoginPolicyMultiFactorValidation(mfa.MFAType)
agg.SetPrecondition(validationQuery, validation) agg.SetPrecondition(validationQuery, validation)
return agg.AppendEvent(model.LoginPolicyMultiFactorAdded, mfa) return agg.AppendEvent(model.LoginPolicyMultiFactorAdded, mfa)
} }
@ -261,7 +261,7 @@ func checkExistingLoginPolicySecondFactorValidation(mfaType int32) func(...*es_m
if err != nil { if err != nil {
return err return err
} }
mfas = append(mfas, mfa.MfaType) mfas = append(mfas, mfa.MFAType)
case model.LoginPolicySecondFactorRemoved: case model.LoginPolicySecondFactorRemoved:
idp := new(iam_es_model.IDPProvider) idp := new(iam_es_model.IDPProvider)
err := idp.SetData(event) err := idp.SetData(event)
@ -301,7 +301,7 @@ func checkExistingLoginPolicyMultiFactorValidation(mfaType int32) func(...*es_mo
if err != nil { if err != nil {
return err return err
} }
mfas = append(mfas, mfa.MfaType) mfas = append(mfas, mfa.MFAType)
case model.LoginPolicyMultiFactorRemoved: case model.LoginPolicyMultiFactorRemoved:
idp := new(iam_es_model.IDPProvider) idp := new(iam_es_model.IDPProvider)
err := idp.SetData(event) err := idp.SetData(event)

View File

@ -472,7 +472,7 @@ func TestLoginPolicySecondFactorAddedAggregate(t *testing.T) {
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
}, },
new: &iam_es_model.MFA{ new: &iam_es_model.MFA{
MfaType: int32(iam_model.SecondFactorTypeOTP), MFAType: int32(iam_model.SecondFactorTypeOTP),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },
@ -562,7 +562,7 @@ func TestLoginPolicySecondFactorRemovedAggregate(t *testing.T) {
}, },
}}, }},
new: &iam_es_model.MFA{ new: &iam_es_model.MFA{
MfaType: int32(iam_model.SecondFactorTypeOTP), MFAType: int32(iam_model.SecondFactorTypeOTP),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },
@ -645,7 +645,7 @@ func TestLoginPolicyMultiFactorAddedAggregate(t *testing.T) {
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
}, },
new: &iam_es_model.MFA{ new: &iam_es_model.MFA{
MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN), MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },
@ -735,7 +735,7 @@ func TestLoginPolicyMultiFactorRemovedAggregate(t *testing.T) {
}, },
}}, }},
new: &iam_es_model.MFA{ new: &iam_es_model.MFA{
MfaType: int32(iam_model.MultiFactorTypeU2FWithPIN), MFAType: int32(iam_model.MultiFactorTypeU2FWithPIN),
}, },
aggCreator: models.NewAggregateCreator("Test"), aggCreator: models.NewAggregateCreator("Test"),
}, },

View File

@ -55,7 +55,7 @@ func (o *Org) appendAddSecondFactorToLoginPolicyEvent(event *es_models.Event) er
if err != nil { if err != nil {
return err return err
} }
o.LoginPolicy.SecondFactors = append(o.LoginPolicy.SecondFactors, mfa.MfaType) o.LoginPolicy.SecondFactors = append(o.LoginPolicy.SecondFactors, mfa.MFAType)
return nil return nil
} }
@ -65,7 +65,7 @@ func (o *Org) appendRemoveSecondFactorFromLoginPolicyEvent(event *es_models.Even
if err != nil { if err != nil {
return err 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[i] = o.LoginPolicy.SecondFactors[len(o.LoginPolicy.SecondFactors)-1]
o.LoginPolicy.SecondFactors[len(o.LoginPolicy.SecondFactors)-1] = 0 o.LoginPolicy.SecondFactors[len(o.LoginPolicy.SecondFactors)-1] = 0
o.LoginPolicy.SecondFactors = o.LoginPolicy.SecondFactors[:len(o.LoginPolicy.SecondFactors)-1] 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 { if err != nil {
return err return err
} }
o.LoginPolicy.MultiFactors = append(o.LoginPolicy.MultiFactors, mfa.MfaType) o.LoginPolicy.MultiFactors = append(o.LoginPolicy.MultiFactors, mfa.MFAType)
return nil return nil
} }
@ -90,7 +90,7 @@ func (o *Org) appendRemoveMultiFactorFromLoginPolicyEvent(event *es_models.Event
if err != nil { if err != nil {
return err 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[i] = o.LoginPolicy.MultiFactors[len(o.LoginPolicy.MultiFactors)-1]
o.LoginPolicy.MultiFactors[len(o.LoginPolicy.MultiFactors)-1] = 0 o.LoginPolicy.MultiFactors[len(o.LoginPolicy.MultiFactors)-1] = 0
o.LoginPolicy.MultiFactors = o.LoginPolicy.MultiFactors[:len(o.LoginPolicy.MultiFactors)-1] o.LoginPolicy.MultiFactors = o.LoginPolicy.MultiFactors[:len(o.LoginPolicy.MultiFactors)-1]

View File

@ -224,7 +224,7 @@ func TestAppendAddSecondFactorToPolicyEvent(t *testing.T) {
name: "append add second factor to login policy event", name: "append add second factor to login policy event",
args: args{ args: args{
org: &Org{LoginPolicy: &iam_es_model.LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, 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{}, event: &es_models.Event{},
}, },
result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ 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) { 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)) 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) 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{ SecondFactors: []int32{
int32(iam_model.SecondFactorTypeOTP), 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{}, event: &es_models.Event{},
}, },
result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{
@ -314,7 +314,7 @@ func TestAppendAddMultiFactorToPolicyEvent(t *testing.T) {
name: "append add mfa to login policy event", name: "append add mfa to login policy event",
args: args{ args: args{
org: &Org{LoginPolicy: &iam_es_model.LoginPolicy{AllowExternalIdp: true, AllowRegister: true, AllowUsernamePassword: true}}, 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{}, event: &es_models.Event{},
}, },
result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ 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) { 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)) 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) 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{ MultiFactors: []int32{
int32(iam_model.MultiFactorTypeU2FWithPIN), 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{}, event: &es_models.Event{},
}, },
result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{ result: &Org{LoginPolicy: &iam_es_model.LoginPolicy{

View File

@ -20,6 +20,7 @@ import (
"github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/id"
proj_model "github.com/caos/zitadel/internal/project/model" proj_model "github.com/caos/zitadel/internal/project/model"
"github.com/caos/zitadel/internal/project/repository/eventsourcing/model" "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/telemetry/tracing"
) )
const ( const (
@ -788,7 +789,9 @@ func (es *ProjectEventstore) ChangeOIDCConfigSecret(ctx context.Context, project
return nil, caos_errs.ThrowInternal(nil, "EVENT-dk87s", "Errors.Internal") 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 == "" { if appID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-H3RT2", "Errors.Project.RequiredFieldsMissing") 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") 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) return es.setOIDCClientSecretCheckResult(ctx, existingProject, app.AppID, OIDCClientSecretCheckSucceededAggregate)
} }
if err := es.setOIDCClientSecretCheckResult(ctx, existingProject, app.AppID, OIDCClientSecretCheckFailedAggregate); err != nil { if err := es.setOIDCClientSecretCheckResult(ctx, existingProject, app.AppID, OIDCClientSecretCheckFailedAggregate); err != nil {

View File

@ -13,6 +13,7 @@ type IAMSetUp struct {
Step5 *Step5 Step5 *Step5
Step6 *Step6 Step6 *Step6
Step7 *Step7 Step7 *Step7
Step8 *Step8
} }
func (setup *IAMSetUp) steps(currentDone iam_model.Step) ([]step, error) { 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.Step5,
setup.Step6, setup.Step6,
setup.Step7, setup.Step7,
setup.Step8,
} { } {
if step.step() <= currentDone { if step.step() <= currentDone {
continue continue

View File

@ -32,23 +32,23 @@ func (step *Step7) init(setup *Setup) {
func (step *Step7) execute(ctx context.Context) (*iam_model.IAM, error) { func (step *Step7) execute(ctx context.Context) (*iam_model.IAM, error) {
iam, agg, err := step.add2FAToPolicy(ctx, step.DefaultSecondFactor) iam, agg, err := step.add2FAToPolicy(ctx, step.DefaultSecondFactor)
if err != nil { 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 return nil, err
} }
iam, agg, push, err := step.setup.IamEvents.PrepareSetupDone(ctx, iam, agg, step.step()) iam, agg, push, err := step.setup.IamEvents.PrepareSetupDone(ctx, iam, agg, step.step())
if err != nil { 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 return nil, err
} }
err = es_sdk.PushAggregates(ctx, push, iam.AppendEvents, agg) err = es_sdk.PushAggregates(ctx, push, iam.AppendEvents, agg)
if err != nil { 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 nil, err
} }
return iam_es_model.IAMToModel(iam), nil 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) { 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) return step.setup.IamEvents.PrepareAddSecondFactorToLoginPolicy(ctx, step.setup.iamID, secondFactor)
} }

54
internal/setup/step8.go Normal file
View File

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

View File

@ -53,12 +53,25 @@ Errors:
IDPConfigNotExisting: IDP Provider ungültig für diese Organisation IDPConfigNotExisting: IDP Provider ungültig für diese Organisation
NotAllowed: Externer IDP ist auf dieser Organisation nicht erlaubt. NotAllowed: Externer IDP ist auf dieser Organisation nicht erlaubt.
MinimumExternalIDPNeeded: Mindestens ein IDP muss hinzugefügt werden. MinimumExternalIDPNeeded: Mindestens ein IDP muss hinzugefügt werden.
Mfa: MFA:
Otp: OTP:
AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet
NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht
NotReady: Multifaktor OTP (OneTimePassword) ist nicht bereit NotReady: Multifaktor OTP (OneTimePassword) ist nicht bereit
InvalidCode: Code ist ungültig 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: Org:
Invalid: Organisation ist ungültig Invalid: Organisation ist ungültig
AlreadyDeactivated: Organisation ist bereits deaktiviert AlreadyDeactivated: Organisation ist bereits deaktiviert

View File

@ -53,12 +53,25 @@ Errors:
IDPConfigNotExisting: IDP provider invalid for this organisation IDPConfigNotExisting: IDP provider invalid for this organisation
NotAllowed: External IDP not allowed on this organisation NotAllowed: External IDP not allowed on this organisation
MinimumExternalIDPNeeded: At least one IDP must be added MinimumExternalIDPNeeded: At least one IDP must be added
Mfa: MFA:
Otp: OTP:
AlreadyReady: Multifactor OTP (OneTimePassword) is already set up AlreadyReady: Multifactor OTP (OneTimePassword) is already set up
NotExisting: Multifactor OTP (OneTimePassword) doesn't exist NotExisting: Multifactor OTP (OneTimePassword) doesn't exist
NotReady: Multifactor OTP (OneTimePassword) isn't ready NotReady: Multifactor OTP (OneTimePassword) isn't ready
InvalidCode: Invalid code 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: Org:
Invalid: Organisation is invalid Invalid: Organisation is invalid
AlreadyDeactivated: Organisation is already deactivated AlreadyDeactivated: Organisation is already deactivated

View File

@ -2,7 +2,6 @@ package handler
import ( import (
"context" "context"
"github.com/caos/zitadel/internal/config/systemdefaults"
"net" "net"
"net/http" "net/http"
@ -16,6 +15,7 @@ import (
"github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/api/http/middleware"
auth_repository "github.com/caos/zitadel/internal/auth/repository" auth_repository "github.com/caos/zitadel/internal/auth/repository"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing" "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/crypto"
"github.com/caos/zitadel/internal/form" "github.com/caos/zitadel/internal/form"
"github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/id"

View File

@ -7,15 +7,15 @@ import (
) )
const ( const (
tmplMfaInitDone = "mfainitdone" tmplMFAInitDone = "mfainitdone"
) )
type mfaInitDoneData struct { 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 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) 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)
} }

View File

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

View File

@ -12,17 +12,17 @@ import (
) )
const ( const (
tmplMfaInitVerify = "mfainitverify" tmplMFAInitVerify = "mfainitverify"
) )
type mfaInitVerifyData struct { type mfaInitVerifyData struct {
MfaType model.MFAType `schema:"mfaType"` MFAType model.MFAType `schema:"mfaType"`
Code string `schema:"code"` Code string `schema:"code"`
URL string `schema:"url"` URL string `schema:"url"`
Secret string `schema:"secret"` 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) data := new(mfaInitVerifyData)
authReq, err := l.getAuthRequestAndParseData(r, data) authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil { if err != nil {
@ -30,29 +30,29 @@ func (l *Login) handleMfaInitVerify(w http.ResponseWriter, r *http.Request) {
return return
} }
var verifyData *mfaVerifyData var verifyData *mfaVerifyData
switch data.MfaType { switch data.MFAType {
case model.MFATypeOTP: case model.MFATypeOTP:
verifyData = l.handleOtpVerify(w, r, authReq, data) verifyData = l.handleOTPVerify(w, r, authReq, data)
} }
if verifyData != nil { if verifyData != nil {
l.renderMfaInitVerify(w, r, authReq, verifyData, err) l.renderMFAInitVerify(w, r, authReq, verifyData, err)
return return
} }
done := &mfaDoneData{ 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 { 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) err := l.authRepo.VerifyMFAOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code)
if err == nil { if err == nil {
return nil return nil
} }
mfadata := &mfaVerifyData{ mfadata := &mfaVerifyData{
MfaType: data.MfaType, MFAType: data.MFAType,
otpData: otpData{ otpData: otpData{
Secret: data.Secret, Secret: data.Secret,
Url: data.URL, Url: data.URL,
@ -62,21 +62,21 @@ func (l *Login) handleOtpVerify(w http.ResponseWriter, r *http.Request, authReq
return mfadata 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 var errType, errMessage string
if err != nil { if err != nil {
errMessage = l.getErrorMessage(r, err) 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) data.profileData = l.getProfileData(authReq)
if data.MfaType == model.MFATypeOTP { if data.MFAType == model.MFATypeOTP {
code, err := generateQrCode(data.otpData.Url) code, err := generateQrCode(data.otpData.Url)
if err == nil { if err == nil {
data.otpData.QrCode = code 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) { func generateQrCode(url string) (string, error) {

View File

@ -8,15 +8,15 @@ import (
) )
const ( const (
tmplMfaPrompt = "mfaprompt" tmplMFAPrompt = "mfaprompt"
) )
type mfaPromptData struct { type mfaPromptData struct {
MfaProvider model.MFAType `schema:"provider"` MFAProvider model.MFAType `schema:"provider"`
Skip bool `schema:"skip"` 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) data := new(mfaPromptData)
authReq, err := l.getAuthRequestAndParseData(r, data) authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil { if err != nil {
@ -25,11 +25,11 @@ func (l *Login) handleMfaPrompt(w http.ResponseWriter, r *http.Request) {
} }
if !data.Skip { if !data.Skip {
mfaVerifyData := new(mfaVerifyData) mfaVerifyData := new(mfaVerifyData)
mfaVerifyData.MfaType = data.MfaProvider mfaVerifyData.MFAType = data.MFAProvider
l.handleMfaCreation(w, r, authReq, mfaVerifyData) l.handleMFACreation(w, r, authReq, mfaVerifyData)
return 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 { if err != nil {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
@ -37,7 +37,7 @@ func (l *Login) handleMfaPrompt(w http.ResponseWriter, r *http.Request) {
l.handleLogin(w, r) 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) data := new(mfaPromptData)
authReq, err := l.getAuthRequestAndParseData(r, data) authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil { if err != nil {
@ -48,45 +48,48 @@ func (l *Login) handleMfaPromptSelection(w http.ResponseWriter, r *http.Request)
l.renderNextStep(w, r, authReq) 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 var errType, errMessage string
if err != nil { if err != nil {
errMessage = l.getErrorMessage(r, err) errMessage = l.getErrorMessage(r, err)
} }
data := mfaData{ data := mfaData{
baseData: l.getBaseData(r, authReq, "Mfa Prompt", errType, errMessage), baseData: l.getBaseData(r, authReq, "MFA Prompt", errType, errMessage),
profileData: l.getProfileData(authReq), profileData: l.getProfileData(authReq),
} }
if mfaPromptData == nil { 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 return
} }
data.MfaProviders = mfaPromptData.MfaProviders data.MFAProviders = mfaPromptData.MFAProviders
data.MfaRequired = mfaPromptData.Required data.MFARequired = mfaPromptData.Required
if len(mfaPromptData.MfaProviders) == 1 && mfaPromptData.Required { if len(mfaPromptData.MFAProviders) == 1 && mfaPromptData.Required {
data := &mfaVerifyData{ data := &mfaVerifyData{
MfaType: mfaPromptData.MfaProviders[0], MFAType: mfaPromptData.MFAProviders[0],
} }
l.handleMfaCreation(w, r, authReq, data) l.handleMFACreation(w, r, authReq, data)
return 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) { func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData) {
switch data.MfaType { switch data.MFAType {
case model.MFATypeOTP: 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 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) { 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) otp, err := l.authRepo.AddMFAOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID)
if err != nil { if err != nil {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
@ -96,5 +99,5 @@ func (l *Login) handleOtpCreation(w http.ResponseWriter, r *http.Request, authRe
Secret: otp.SecretString, Secret: otp.SecretString,
Url: otp.Url, Url: otp.Url,
} }
l.renderMfaInitVerify(w, r, authReq, data, nil) l.renderMFAInitVerify(w, r, authReq, data, nil)
} }

View File

@ -8,24 +8,24 @@ import (
) )
const ( const (
tmplMfaVerify = "mfaverify" tmplMFAVerify = "mfaverify"
) )
type mfaVerifyFormData struct { type mfaVerifyFormData struct {
MfaType model.MFAType `schema:"mfaType"` MFAType model.MFAType `schema:"mfaType"`
Code string `schema:"code"` 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) data := new(mfaVerifyFormData)
authReq, err := l.getAuthRequestAndParseData(r, data) authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil { if err != nil {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
} }
if data.MfaType == model.MFATypeOTP { if data.MFAType == model.MFATypeOTP {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) 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 { if err != nil {
l.renderError(w, r, authReq, err) 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) 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 var errType, errMessage string
if err != nil { if err != nil {
errMessage = l.getErrorMessage(r, err) errMessage = l.getErrorMessage(r, err)
} }
data := l.getUserData(r, authReq, "Mfa Verify", errType, errMessage) data := l.getUserData(r, authReq, "MFA Verify", errType, errMessage)
if verificationStep != nil { if verificationStep == nil {
data.MfaProviders = verificationStep.MfaProviders l.renderError(w, r, authReq, err)
data.SelectedMfaProvider = verificationStep.MfaProviders[0] 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)
} }

View File

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

View File

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

View File

@ -3,18 +3,19 @@ package handler
import ( import (
"errors" "errors"
"fmt" "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" "html/template"
"net/http" "net/http"
"path" "path"
"github.com/caos/logging"
"github.com/gorilla/csrf"
"golang.org/x/text/language"
http_mw "github.com/caos/zitadel/internal/api/http/middleware" http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/auth_request/model"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/i18n" "github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/renderer" "github.com/caos/zitadel/internal/renderer"
) )
@ -32,31 +33,34 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
pathPrefix: pathPrefix, pathPrefix: pathPrefix,
} }
tmplMapping := map[string]string{ tmplMapping := map[string]string{
tmplError: "error.html", tmplError: "error.html",
tmplLogin: "login.html", tmplLogin: "login.html",
tmplUserSelection: "select_user.html", tmplUserSelection: "select_user.html",
tmplPassword: "password.html", tmplPassword: "password.html",
tmplMfaVerify: "mfa_verify.html", tmplPasswordlessVerification: "passwordless.html",
tmplMfaPrompt: "mfa_prompt.html", tmplMFAVerify: "mfa_verify.html",
tmplMfaInitVerify: "mfa_init_verify.html", tmplMFAPrompt: "mfa_prompt.html",
tmplMfaInitDone: "mfa_init_done.html", tmplMFAInitVerify: "mfa_init_verify.html",
tmplMailVerification: "mail_verification.html", tmplMFAU2FInit: "mfa_init_u2f.html",
tmplMailVerified: "mail_verified.html", tmplU2FVerification: "mfa_verification_u2f.html",
tmplInitPassword: "init_password.html", tmplMFAInitDone: "mfa_init_done.html",
tmplInitPasswordDone: "init_password_done.html", tmplMailVerification: "mail_verification.html",
tmplInitUser: "init_user.html", tmplMailVerified: "mail_verified.html",
tmplInitUserDone: "init_user_done.html", tmplInitPassword: "init_password.html",
tmplPasswordResetDone: "password_reset_done.html", tmplInitPasswordDone: "init_password_done.html",
tmplChangePassword: "change_password.html", tmplInitUser: "init_user.html",
tmplChangePasswordDone: "change_password_done.html", tmplInitUserDone: "init_user_done.html",
tmplRegisterOption: "register_option.html", tmplPasswordResetDone: "password_reset_done.html",
tmplRegister: "register.html", tmplChangePassword: "change_password.html",
tmplLogoutDone: "logout_done.html", tmplChangePasswordDone: "change_password_done.html",
tmplRegisterOrg: "register_org.html", tmplRegisterOption: "register_option.html",
tmplChangeUsername: "change_username.html", tmplRegister: "register.html",
tmplChangeUsernameDone: "change_username_done.html", tmplLogoutDone: "logout_done.html",
tmplLinkUsersDone: "link_users_done.html", tmplRegisterOrg: "register_org.html",
tmplExternalNotFoundOption: "external_not_found_option.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{}{ funcs := map[string]interface{}{
"resourceUrl": func(file string) string { "resourceUrl": func(file string) string {
@ -86,6 +90,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
"userSelectionUrl": func() string { "userSelectionUrl": func() string {
return path.Join(r.pathPrefix, EndpointUserSelection) return path.Join(r.pathPrefix, EndpointUserSelection)
}, },
"passwordLessVerificationUrl": func() string {
return path.Join(r.pathPrefix, EndpointPasswordlessLogin)
},
"passwordResetUrl": func(id string) string { "passwordResetUrl": func(id string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointPasswordReset, queryAuthRequestID, id)) 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) return path.Join(r.pathPrefix, EndpointPassword)
}, },
"mfaVerifyUrl": func() string { "mfaVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMfaVerify) return path.Join(r.pathPrefix, EndpointMFAVerify)
}, },
"mfaPromptUrl": func() string { "mfaPromptUrl": func() string {
return path.Join(r.pathPrefix, EndpointMfaPrompt) return path.Join(r.pathPrefix, EndpointMFAPrompt)
}, },
"mfaPromptChangeUrl": func(id string, provider model.MFAType) string { "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 { "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 { "mailVerificationUrl": func() string {
return path.Join(r.pathPrefix, EndpointMailVerification) 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) l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
case *model.PasswordStep: case *model.PasswordStep:
l.renderPassword(w, r, authReq, nil) l.renderPassword(w, r, authReq, nil)
case *model.MfaVerificationStep: case *model.PasswordlessStep:
l.renderMfaVerify(w, r, authReq, step, err) l.renderPasswordlessVerification(w, r, authReq, nil)
case *model.MFAVerificationStep:
l.renderMFAVerify(w, r, authReq, step, err)
case *model.RedirectToCallbackStep: case *model.RedirectToCallbackStep:
if len(authReq.PossibleSteps) > 1 { if len(authReq.PossibleSteps) > 1 {
l.chooseNextStep(w, r, authReq, 1, err) 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) l.renderChangePassword(w, r, authReq, err)
case *model.VerifyEMailStep: case *model.VerifyEMailStep:
l.renderMailVerification(w, r, authReq, "", err) l.renderMailVerification(w, r, authReq, "", err)
case *model.MfaPromptStep: case *model.MFAPromptStep:
l.renderMfaPrompt(w, r, authReq, step, err) l.renderMFAPrompt(w, r, authReq, step, err)
case *model.InitUserStep: case *model.InitUserStep:
l.renderInitUser(w, r, authReq, "", "", step.PasswordSet, nil) l.renderInitUser(w, r, authReq, "", "", step.PasswordSet, nil)
case *model.ChangeUsernameStep: case *model.ChangeUsernameStep:
@ -356,8 +371,8 @@ type userData struct {
baseData baseData
profileData profileData
PasswordChecked string PasswordChecked string
MfaProviders []model.MFAType MFAProviders []model.MFAType
SelectedMfaProvider model.MFAType SelectedMFAProvider model.MFAType
Linking bool Linking bool
} }
@ -386,21 +401,21 @@ type userSelectionData struct {
type mfaData struct { type mfaData struct {
baseData baseData
profileData profileData
MfaProviders []model.MFAType MFAProviders []model.MFAType
MfaRequired bool MFARequired bool
} }
type mfaVerifyData struct { type mfaVerifyData struct {
baseData baseData
profileData profileData
MfaType model.MFAType MFAType model.MFAType
otpData otpData
} }
type mfaDoneData struct { type mfaDoneData struct {
baseData baseData
profileData profileData
MfaType model.MFAType MFAType model.MFAType
} }
type otpData struct { type otpData struct {

View File

@ -13,6 +13,7 @@ const (
EndpointLogin = "/login" EndpointLogin = "/login"
EndpointExternalLogin = "/login/externalidp" EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback" EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointPasswordlessLogin = "/login/passwordless"
EndpointLoginName = "/loginname" EndpointLoginName = "/loginname"
EndpointUserSelection = "/userselection" EndpointUserSelection = "/userselection"
EndpointChangeUsername = "/username/change" EndpointChangeUsername = "/username/change"
@ -21,9 +22,11 @@ const (
EndpointChangePassword = "/password/change" EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset" EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init" EndpointInitUser = "/user/init"
EndpointMfaVerify = "/mfa/verify" EndpointMFAVerify = "/mfa/verify"
EndpointMfaPrompt = "/mfa/prompt" EndpointMFAPrompt = "/mfa/prompt"
EndpointMfaInitVerify = "/mfa/init/verify" EndpointMFAInitVerify = "/mfa/init/verify"
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
EndpointU2FVerification = "/mfa/u2f/verify"
EndpointMailVerification = "/mail/verification" EndpointMailVerification = "/mail/verification"
EndpointMailVerified = "/mail/verified" EndpointMailVerified = "/mail/verified"
EndpointRegisterOption = "/register/option" 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(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).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.handleLoginName).Methods(http.MethodGet)
router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost) router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointUserSelection, login.handleSelectUser).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(EndpointPasswordReset, login.handlePasswordReset).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUser).Methods(http.MethodGet) router.HandleFunc(EndpointInitUser, login.handleInitUser).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUserCheck).Methods(http.MethodPost) router.HandleFunc(EndpointInitUser, login.handleInitUserCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointMfaVerify, login.handleMfaVerify).Methods(http.MethodPost) router.HandleFunc(EndpointMFAVerify, login.handleMFAVerify).Methods(http.MethodPost)
router.HandleFunc(EndpointMfaPrompt, login.handleMfaPromptSelection).Methods(http.MethodGet) router.HandleFunc(EndpointMFAPrompt, login.handleMFAPromptSelection).Methods(http.MethodGet)
router.HandleFunc(EndpointMfaPrompt, login.handleMfaPrompt).Methods(http.MethodPost) router.HandleFunc(EndpointMFAPrompt, login.handleMFAPrompt).Methods(http.MethodPost)
router.HandleFunc(EndpointMfaInitVerify, login.handleMfaInitVerify).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.handleMailVerification).Methods(http.MethodGet)
router.HandleFunc(EndpointMailVerification, login.handleMailVerificationCheck).Methods(http.MethodPost) router.HandleFunc(EndpointMailVerification, login.handleMailVerificationCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointChangePassword, login.handleChangePassword).Methods(http.MethodPost) router.HandleFunc(EndpointChangePassword, login.handleChangePassword).Methods(http.MethodPost)

View File

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

View File

@ -35,10 +35,10 @@ UsernameChangeDone:
Title: Username geändert Title: Username geändert
Description: Der Username wurde erfolgreich geändert. Description: Der Username wurde erfolgreich geändert.
MfaVerify: MFAVerify:
Title: Multifaktor verifizieren Title: Multifaktor verifizieren
Description: Verifiziere deinen Multifaktor Description: Verifiziere deinen Multifaktor
OTP: OTP OTP: OTP (One Time Password)
Code: Code Code: Code
InitPassword: InitPassword:
@ -63,23 +63,41 @@ InitUserDone:
Title: User aktiviert Title: User aktiviert
Description: EMail verifiziert und Passwort erfolgreich gesetzt Description: EMail verifiziert und Passwort erfolgreich gesetzt
MfaPrompt: MFAPrompt:
Title: Multifaktor hinzufügen Title: Multifaktor hinzufügen
Description: Möchtest du einen Mulitfaktor hinzufügen? Description: Möchtest du einen Mulitfaktor hinzufügen?
Provider0: OTP (One Time Password) Provider0: OTP (One Time Password)
Provider1: U2F (Universal 2nd Factor) Provider1: U2F (Universal 2nd Factor)
MfaInitVerify: MFAInitVerify:
Title: Multifaktor Verifizierung Title: Multifaktor Verifizierung
Description: Verifiziere deinen Multifaktor 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 Secret: Secret
Code: Code Code: Code
MfaInitDone: MFAInitDone:
Title: Multifaktor Verifizierung erstellt Title: Multifaktor Verifizierung erstellt
Description: Multifikator Verifizierung erfolgreich abgeschlossen. Der Multifaktor muss bei jeder Anmeldung eingegeben werden, dies beinhaltet auch den aktuellen Authentifizierungs Prozess. 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: PasswordChange:
Title: Passwort ändern Title: Passwort ändern
Description: Ändere dein Password in dem du dein altes und dann dein neuen Passwort eingibst. Description: Ändere dein Password in dem du dein altes und dann dein neuen Passwort eingibst.
@ -181,6 +199,9 @@ Actions:
ForgotPassword: Password zurücksetzen ForgotPassword: Password zurücksetzen
Cancel: Abbrechen Cancel: Abbrechen
Save: speichern Save: speichern
RegisterToken: Token registrieren
ValidateToken: Token validieren
Recreate: erneut erstellen
Errors: Errors:
Internal: Es ist ein interner Fehler aufgetreten Internal: Es ist ein interner Fehler aufgetreten
@ -215,9 +236,9 @@ Errors:
GeneratorAlgNotSupported: Generator Algorithums wird nicht unterstützt GeneratorAlgNotSupported: Generator Algorithums wird nicht unterstützt
EmailVerify: EmailVerify:
UserIDEmpty: UserID ist leer UserIDEmpty: UserID ist leer
Mfa: MFA:
NoProviders: Es stehen keine Multifaktorprovider zur Verfügung NoProviders: Es stehen keine Multifaktorprovider zur Verfügung
Otp: OTP:
AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet
NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht
InvalidCode: Code ist ungültig InvalidCode: Code ist ungültig

View File

@ -35,10 +35,10 @@ UsernameChangeDone:
Title: Username changed Title: Username changed
Description: Your username was changed successfully. Description: Your username was changed successfully.
MfaVerify: MFAVerify:
Title: Verify Multificator Title: Verify Multificator
Description: Verify your multifactor Description: Verify your multifactor
OTP: OTP OTP: OTP (One Time Password)
Code: Code Code: Code
InitPassword: InitPassword:
@ -63,23 +63,41 @@ InitUserDone:
Title: User activated Title: User activated
Description: Email verified and Password successfully set Description: Email verified and Password successfully set
MfaPrompt: MFAPrompt:
Title: Multifactor Setup Title: Multifactor Setup
Description: Would you like to setup multifactor authentication? Description: Would you like to setup multifactor authentication?
Provider0: OTP (One Time Password) Provider0: OTP (One Time Password)
Provider1: U2F (Universal 2nd Factor) Provider1: U2F (Universal 2nd Factor)
MfaInitVerify: MFAInitVerify:
Title: Multifactor Verification Title: Multifactor Verification
Description: Verify your multifactor. 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 Secret: Secret
Code: Code Code: Code
MfaInitDone: MFAInitDone:
Title: Multifcator Verification done Title: Multifcator Verification done
Description: Multifactor verification successfully done. The multifactor has to be entered on each login, even in the actual authentification process. 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: PasswordChange:
Title: Change Password Title: Change Password
Description: Change your password. Enter your old and new password. Description: Change your password. Enter your old and new password.
@ -181,6 +199,9 @@ Actions:
ForgotPassword: reset password ForgotPassword: reset password
Cancel: cancel Cancel: cancel
Save: save Save: save
RegisterToken: Register Token
ValidateToken: Validate Token
Recreate: recreate
Errors: Errors:
Internal: An internal error occured Internal: An internal error occured
@ -215,9 +236,9 @@ Errors:
GeneratorAlgNotSupported: Unsupported generator algorithm GeneratorAlgNotSupported: Unsupported generator algorithm
EmailVerify: EmailVerify:
UserIDEmpty: UserID is empty UserIDEmpty: UserID is empty
Mfa: MFA:
NoProviders: No available multifactor providers NoProviders: No available multifactor providers
Otp: OTP:
AlreadyReady: Multifactor OTP (OneTimePassword) is already setup AlreadyReady: Multifactor OTP (OneTimePassword) is already setup
NotExisting: Multifactor OTP (OneTimePassword) doesn't exist NotExisting: Multifactor OTP (OneTimePassword) doesn't exist
InvalidCode: Invalid code InvalidCode: Invalid code

View File

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

View File

@ -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, "");
}

View File

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

View File

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

View File

@ -487,4 +487,12 @@ footer {
color: #F20D6B; color: #F20D6B;
} }
.hidden {
display: none;
}
#wa-error {
margin-top: 20px;
}
/*# sourceMappingURL=dark.css.map */ /*# sourceMappingURL=dark.css.map */

View File

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

View File

@ -487,6 +487,14 @@ footer {
color: #F20D6B; color: #F20D6B;
} }
.hidden {
display: none;
}
#wa-error {
margin-top: 20px;
}
html { html {
background-color: white; background-color: white;
color: #282828; color: #282828;

View File

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

View File

@ -466,3 +466,11 @@ footer {
.error { .error {
color: $nokColor; color: $nokColor;
} }
.hidden {
display: none;
}
#wa-error {
margin-top: 20px;
}

View File

@ -487,4 +487,12 @@ footer {
color: #F20D6B; color: #F20D6B;
} }
.hidden {
display: none;
}
#wa-error {
margin-top: 20px;
}
/*# sourceMappingURL=dark.css.map */ /*# sourceMappingURL=dark.css.map */

View File

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

View File

@ -487,6 +487,14 @@ footer {
color: #F20D6B; color: #F20D6B;
} }
.hidden {
display: none;
}
#wa-error {
margin-top: 20px;
}
html { html {
background-color: #f5f5f5; background-color: #f5f5f5;
color: #282828; color: #282828;

View File

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

View File

@ -3,7 +3,7 @@
<div class="head"> <div class="head">
{{ template "user-profile" . }} {{ template "user-profile" . }}
<p>{{t "MfaInitDone.Description"}}</p> <p>{{t "MFAInitDone.Description"}}</p>
</div> </div>
<form action="{{ loginUrl }}" method="POST"> <form action="{{ loginUrl }}" method="POST">
@ -11,7 +11,7 @@
{{ .CSRF }} {{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .MfaType }}" /> <input type="hidden" name="mfaType" value="{{ .MFAType }}" />
<div class="actions"> <div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button> <button class="primary right" type="submit">{{t "Actions.Next"}}</button>

View File

@ -0,0 +1,42 @@
{{template "main-top" .}}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "MFAInitU2F.Description"}}</p>
</div>
<form action="{{ mfaInitU2FVerifyUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="credentialCreationData" value="{{ .CredentialCreationData }}" />
<input type="hidden" name="credentialData" />
<div class="fields">
<p class="wa-no-support error hidden">{{t "WebAuthN.NotSupported"}}</p>
<div class="field">
<label class="label" for="name">{{t "WebAuthN.Name"}}</label>
<input class="input" type="text" id="name" name="name" autocomplete="off" autofocus>
</div>
<a id="btn-register" class="button primary wa-support">{{t "Actions.RegisterToken"}}</a>
<div id="wa-error" class="error hidden">
<span class="cause"></span>
<span>{{t "WebAuthN.Error.Retry"}}</span>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button class="secondary right wa-support" id="submit-button" type="submit" name="recreate" value="true">{{t "Actions.Recreate"}}</button>
</div>
</form>
<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_register.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -3,7 +3,7 @@
<div class="head"> <div class="head">
{{ template "user-profile" . }} {{ template "user-profile" . }}
<p>{{t "MfaInitVerify.Description"}}</p> <p>{{t "MFAInitVerify.Description"}}</p>
</div> </div>
<form action="{{ mfaInitVerifyUrl }}" method="POST"> <form action="{{ mfaInitVerifyUrl }}" method="POST">
@ -11,25 +11,25 @@
{{ .CSRF }} {{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .MfaType }}" /> <input type="hidden" name="mfaType" value="{{ .MFAType }}" />
<input type="hidden" name="url" value="{{ .Url }}" /> <input type="hidden" name="url" value="{{ .Url }}" />
<input type="hidden" name="secret" value="{{ .Secret }}" /> <input type="hidden" name="secret" value="{{ .Secret }}" />
{{if (eq .MfaType 0) }} {{if (eq .MFAType 0) }}
<p>{{t "MfaInitVerify.OtpDescription"}}</p> <p>{{t "MFAInitVerify.OTPDescription"}}</p>
<div id="qrcode"> <div id="qrcode">
{{.QrCode}} {{.QrCode}}
</div> </div>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<span class="label" for="secret">{{t "MfaInitVerify.Secret"}}</span> <span class="label" for="secret">{{t "MFAInitVerify.Secret"}}</span>
<span class="input" id="secret"> <span class="input" id="secret">
{{.Secret}} {{.Secret}}
<span class="copy material-icons" data-copy="{{ .Secret }}" >content_copy</span> <span class="copy material-icons" data-copy="{{ .Secret }}" >content_copy</span>
</span> </span>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="code">{{t "MfaInitVerify.Code"}}</label> <label class="label" for="code">{{t "MFAInitVerify.Code"}}</label>
<input class="input" type="text" id="code" name="code" autocomplete="off" autofocus required> <input class="input" type="text" id="code" name="code" autocomplete="off" autofocus required>
</div> </div>
</div> </div>
@ -37,7 +37,7 @@
<div class="actions"> <div class="actions">
<button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button> <button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
<a class="button secondary" href="{{ mfaPromptChangeUrl .AuthReqID .MfaType }}"> <a class="button secondary" href="{{ mfaPromptChangeUrl .AuthReqID .MFAType }}">
{{t "Actions.Back"}} {{t "Actions.Back"}}
</a> </a>
<a class="button secondary" href="{{ loginUrl }}"> <a class="button secondary" href="{{ loginUrl }}">

View File

@ -3,7 +3,7 @@
<div class="head"> <div class="head">
{{ template "user-profile" . }} {{ template "user-profile" . }}
<p>{{t "MfaPrompt.Description"}}</p> <p>{{t "MFAPrompt.Description"}}</p>
</div> </div>
<form action="{{ mfaPromptUrl }}" method="POST"> <form action="{{ mfaPromptUrl }}" method="POST">
@ -13,8 +13,8 @@
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="fields"> <div class="fields">
{{ range $provider := .MfaProviders}} {{ range $provider := .MFAProviders}}
{{ $providerName := (t (printf "MfaPrompt.Provider%v" $provider)) }} {{ $providerName := (t (printf "MFAPrompt.Provider%v" $provider)) }}
<div class="field radio-button"> <div class="field radio-button">
<input id="{{ $provider }}" type="radio" name="provider" value="{{ $provider }}"> <input id="{{ $provider }}" type="radio" name="provider" value="{{ $provider }}">
<label for="{{ $provider }}">{{ $providerName }}</label> <label for="{{ $provider }}">{{ $providerName }}</label>
@ -24,7 +24,7 @@
<div class="actions"> <div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button> <button class="primary right" type="submit">{{t "Actions.Next"}}</button>
{{if not .MfaRequired}} {{if not .MFARequired}}
<button class="default right" name="skip" value="true" type="submit" formnovalidate>{{t "Actions.Skip"}}</button> <button class="default right" name="skip" value="true" type="submit" formnovalidate>{{t "Actions.Skip"}}</button>
{{end}} {{end}}
<a class="button secondary" href="{{ loginUrl }}"> <a class="button secondary" href="{{ loginUrl }}">

View File

@ -0,0 +1,37 @@
{{template "main-top" .}}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "MFAVerifyU2F.Description"}}</p>
</div>
<form action="{{ mfaInitU2FLoginUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}"/>
<input type="hidden" name="credentialAssertionData" value="{{ .CredentialCreationData }}"/>
<input type="hidden" name="credentialData"/>
<div id="webauthn">
<p class="wa-no-support error hidden">{{t "WebAuthN.NotSupported"}}</p>
<a id="btn-login" class="button primary wa-support">{{t "Actions.ValidateToken"}}</a>
<div id="wa-error" class="error hidden">
<span class="cause"></span>
<span>{{t "WebAuthN.Error.Retry"}}</span>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button class="secondary right wa-support" id="submit-button" type="submit" name="recreate" value="true">{{t "Actions.Recreate"}}</button>
</div>
</form>
<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_login.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -3,7 +3,7 @@
<div class="head"> <div class="head">
{{ template "user-profile" . }} {{ template "user-profile" . }}
<p>{{t "MfaVerify.Description"}}</p> <p>{{t "MFAVerify.Description"}}</p>
</div> </div>
<form action="{{ mfaVerifyUrl }}" method="POST"> <form action="{{ mfaVerifyUrl }}" method="POST">
@ -11,11 +11,11 @@
{{ .CSRF }} {{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .SelectedMfaProvider }}" /> <input type="hidden" name="mfaType" value="{{ .SelectedMFAProvider }}" />
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<label class="label" for="code">{{t "MfaVerify.Code"}}</label> <label class="label" for="code">{{t "MFAVerify.Code"}}</label>
<input class="input" type="text" id="code" name="code" autocomplete="off" autofocus required> <input class="input" type="text" id="code" name="code" autocomplete="off" autofocus required>
</div> </div>
</div> </div>

View File

@ -0,0 +1,37 @@
{{template "main-top" .}}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "Passwordless.Description"}}</p>
</div>
<form action="{{ passwordLessVerificationUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}"/>
<input type="hidden" name="credentialAssertionData" value="{{ .CredentialCreationData }}"/>
<input type="hidden" name="credentialData"/>
<div id="webauthn">
<p class="wa-no-support error hidden">{{t "WebAuthN.NotSupported"}}</p>
<a id="btn-login" class="button primary wa-support">{{t "Actions.ValidateToken"}}</a>
<div id="wa-error" class="error hidden">
<span class="cause"></span>
<span>{{t "WebAuthN.Error.Retry"}}</span>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button class="secondary right wa-support" id="submit-button" type="submit" name="recreate" value="true">{{t "Actions.Recreate"}}</button>
</div>
</form>
<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_login.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -11,26 +11,27 @@ type OTP struct {
Secret *crypto.CryptoValue Secret *crypto.CryptoValue
SecretString string SecretString string
Url string Url string
State MfaState State MFAState
} }
type MfaState int32 type MFAState int32
const ( const (
MfaStateUnspecified MfaState = iota MFAStateUnspecified MFAState = iota
MfaStateNotReady MFAStateNotReady
MfaStateReady MFAStateReady
) )
type MultiFactor struct { type MultiFactor struct {
Type MfaType Type MFAType
State MfaState State MFAState
Attribute string
} }
type MfaType int32 type MFAType int32
const ( const (
MfaTypeUnspecified MfaType = iota MFATypeUnspecified MFAType = iota
MfaTypeOTP MFATypeOTP
MfaTypeSMS MFATypeU2F
) )

View File

@ -1,9 +1,11 @@
package model package model
import ( import (
iam_model "github.com/caos/zitadel/internal/iam/model" "bytes"
"time" "time"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models" es_models "github.com/caos/zitadel/internal/eventstore/models"
) )
@ -14,12 +16,16 @@ type Human struct {
*Email *Email
*Phone *Phone
*Address *Address
ExternalIDPs []*ExternalIDP ExternalIDPs []*ExternalIDP
InitCode *InitUserCode InitCode *InitUserCode
EmailCode *EmailCode EmailCode *EmailCode
PhoneCode *PhoneCode PhoneCode *PhoneCode
PasswordCode *PasswordCode PasswordCode *PasswordCode
OTP *OTP OTP *OTP
U2FTokens []*WebAuthNToken
PasswordlessTokens []*WebAuthNToken
U2FLogins []*WebAuthNLogin
PasswordlessLogins []*WebAuthNLogin
} }
type InitUserCode struct { type InitUserCode struct {
@ -53,7 +59,7 @@ func (u *Human) IsInitialState() bool {
} }
func (u *Human) IsOTPReady() 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 { 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 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
}

View File

@ -19,6 +19,7 @@ type UserSessionView struct {
DisplayName string DisplayName string
SelectedIDPConfigID string SelectedIDPConfigID string
PasswordVerification time.Time PasswordVerification time.Time
PasswordlessVerification time.Time
ExternalLoginVerification time.Time ExternalLoginVerification time.Time
SecondFactorVerification time.Time SecondFactorVerification time.Time
SecondFactorVerificationType req_model.MFAType SecondFactorVerificationType req_model.MFAType

View File

@ -1,14 +1,15 @@
package model package model
import ( import (
iam_model "github.com/caos/zitadel/internal/iam/model"
"time" "time"
"golang.org/x/text/language"
req_model "github.com/caos/zitadel/internal/auth_request/model" req_model "github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models" "github.com/caos/zitadel/internal/eventstore/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/model" "github.com/caos/zitadel/internal/model"
"golang.org/x/text/language"
) )
type UserView struct { type UserView struct {
@ -46,12 +47,20 @@ type HumanView struct {
PostalCode string PostalCode string
Region string Region string
StreetAddress string StreetAddress string
OTPState MfaState OTPState MFAState
MfaMaxSetUp req_model.MFALevel U2FTokens []*WebAuthNView
MfaInitSkipped time.Time PasswordlessTokens []*WebAuthNView
MFAMaxSetUp req_model.MFALevel
MFAInitSkipped time.Time
InitRequired bool InitRequired bool
} }
type WebAuthNView struct {
TokenID string
Name string
State MFAState
}
type MachineView struct { type MachineView struct {
LastKeyAdded time.Time LastKeyAdded time.Time
Name string 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}) 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) types := make([]req_model.MFAType, 0)
switch level { switch level {
default: default:
@ -118,13 +127,14 @@ func (u *UserView) MfaTypesSetupPossible(level req_model.MFALevel, policy *iam_m
for _, mfaType := range policy.SecondFactors { for _, mfaType := range policy.SecondFactors {
switch mfaType { switch mfaType {
case iam_model.SecondFactorTypeOTP: case iam_model.SecondFactorTypeOTP:
if u.OTPState != MfaStateReady { if u.OTPState != MFAStateReady {
types = append(types, req_model.MFATypeOTP) types = append(types, req_model.MFATypeOTP)
} }
case iam_model.SecondFactorTypeU2F:
types = append(types, req_model.MFATypeU2F)
} }
} }
} }
//PLANNED: add sms //PLANNED: add sms
fallthrough fallthrough
case req_model.MFALevelMultiFactor: case req_model.MFALevelMultiFactor:
@ -132,17 +142,15 @@ func (u *UserView) MfaTypesSetupPossible(level req_model.MFALevel, policy *iam_m
for _, mfaType := range policy.MultiFactors { for _, mfaType := range policy.MultiFactors {
switch mfaType { switch mfaType {
case iam_model.MultiFactorTypeU2FWithPIN: case iam_model.MultiFactorTypeU2FWithPIN:
// TODO: Check if not set up already types = append(types, req_model.MFATypeU2FUserVerification)
// types = append(types, req_model.MFATypeU2F)
} }
} }
} }
//PLANNED: add token
} }
return types 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) types := make([]req_model.MFAType, 0)
required := true required := true
switch level { switch level {
@ -154,9 +162,13 @@ func (u *UserView) MfaTypesAllowed(level req_model.MFALevel, policy *iam_model.L
for _, mfaType := range policy.SecondFactors { for _, mfaType := range policy.SecondFactors {
switch mfaType { switch mfaType {
case iam_model.SecondFactorTypeOTP: case iam_model.SecondFactorTypeOTP:
if u.OTPState == MfaStateReady { if u.OTPState == MFAStateReady {
types = append(types, req_model.MFATypeOTP) 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 { for _, mfaType := range policy.MultiFactors {
switch mfaType { switch mfaType {
case iam_model.MultiFactorTypeU2FWithPIN: case iam_model.MultiFactorTypeU2FWithPIN:
// TODO: Check if not set up already if u.IsPasswordlessReady() {
// types = append(types, req_model.MFATypeU2F) 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 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 { func (u *UserView) HasRequiredOrgMFALevel(policy *iam_model.LoginPolicyView) bool {
if !policy.ForceMFA { if !policy.ForceMFA {
return true return true
} }
switch u.MfaMaxSetUp { switch u.MFAMaxSetUp {
case req_model.MFALevelSecondFactor: case req_model.MFALevelSecondFactor:
return policy.HasSecondFactors() return policy.HasSecondFactors()
case req_model.MFALevelMultiFactor: case req_model.MFALevelMultiFactor:
return true return policy.HasMultiFactors()
default: default:
return false return false
} }

View File

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

View File

@ -24,6 +24,7 @@ import (
"github.com/caos/zitadel/internal/telemetry/tracing" "github.com/caos/zitadel/internal/telemetry/tracing"
usr_model "github.com/caos/zitadel/internal/user/model" usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
webauthn_helper "github.com/caos/zitadel/internal/webauthn"
) )
const ( const (
@ -45,6 +46,7 @@ type UserEventstore struct {
MachineKeySize int MachineKeySize int
Multifactors global_model.Multifactors Multifactors global_model.Multifactors
validateTOTP func(string, string) bool validateTOTP func(string, string) bool
webauthn *webauthn_helper.WebAuthN
} }
type UserConfig struct { type UserConfig struct {
@ -66,9 +68,12 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto
emailVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.EmailVerificationCode, aesCrypto) emailVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.EmailVerificationCode, aesCrypto)
phoneVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PhoneVerificationCode, aesCrypto) phoneVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PhoneVerificationCode, aesCrypto)
passwordVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PasswordVerificationCode, 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) 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{ return &UserEventstore{
Eventstore: conf.Eventstore, Eventstore: conf.Eventstore,
userCache: userCache, userCache: userCache,
@ -80,7 +85,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto
PasswordVerificationCode: passwordVerificationCode, PasswordVerificationCode: passwordVerificationCode,
Multifactors: global_model.Multifactors{ Multifactors: global_model.Multifactors{
OTP: global_model.OTP{ OTP: global_model.OTP{
CryptoMFA: aesOtpCrypto, CryptoMFA: aesOTPCrypto,
Issuer: systemDefaults.Multifactors.OTP.Issuer, Issuer: systemDefaults.Multifactors.OTP.Issuer,
}, },
}, },
@ -88,6 +93,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto
validateTOTP: totp.Validate, validateTOTP: totp.Validate,
MachineKeyAlg: aesCrypto, MachineKeyAlg: aesCrypto,
MachineKeySize: int(systemDefaults.SecretGenerators.MachineKeySize), MachineKeySize: int(systemDefaults.SecretGenerators.MachineKeySize),
webauthn: web,
}, nil }, nil
} }
@ -109,6 +115,20 @@ func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.U
return model.UserToModel(user), nil 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) { func (es *UserEventstore) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) {
query, err := UserByIDQuery(id, sequence) query, err := UserByIDQuery(id, sequence)
if err != nil { 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) { func (es *UserEventstore) InitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d8diw", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-mDPtj", "Errors.User.NotHuman")
}
if user.InitCode != nil { if user.InitCode != nil {
return 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) { func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-9bbXj", "Errors.User.NotHuman")
}
initCode := new(usr_model.InitUserCode) initCode := new(usr_model.InitUserCode)
err = initCode.GenerateInitUserCode(es.InitializeUserCode) 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 { func (es *UserEventstore) InitCodeSent(ctx context.Context, userID string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-0posw", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-SvPa6", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
agg := UserInitCodeSentAggregate(es.AggregateCreator(), repoUser) 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 { func (es *UserEventstore) VerifyInitCode(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, verificationCode, password string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "Errors.User.UserIDMissing") if err != nil {
return err
} }
if verificationCode == "" { if verificationCode == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "Errors.User.Code.Empty") return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "Errors.User.Code.Empty")
} }
pw := &usr_model.Password{SecretString: password} pw := &usr_model.Password{SecretString: password}
err := pw.HashPasswordIfExisting(policy, es.PasswordAlg, false) err = pw.HashPasswordIfExisting(policy, es.PasswordAlg, false)
if err != nil { if err != nil {
return err 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 { if user.InitCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-spo9W", "Errors.User.Code.NotFound") 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 return nil
} }
func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error { func (es *UserEventstore) SkipMFAInit(ctx context.Context, userID string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-S1tdl", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
agg := SkipMfaAggregate(es.AggregateCreator(), repoUser) agg := SkipMFAAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg) err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil { if err != nil {
return err 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) { func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) (*usr_model.Password, error) {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-jLHYG", "Errors.User.NotHuman")
}
if user.Password != nil { if user.Password != nil {
return 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) { func (es *UserEventstore) CheckPassword(ctx context.Context, userID, password string, authRequest *req_model.AuthRequest) (err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
user, err := es.UserByID(ctx, userID) user, err := es.HumanByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-HxcAx", "Errors.User.NotHuman")
}
if user.Password == nil { if user.Password == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s35Fa", "Errors.User.Password.Empty") 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) { 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 { if err != nil {
return nil, err 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) 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 { 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 { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-pHkAQ", "Errors.User.NotHuman")
}
if user.PasswordCode == nil { if user.PasswordCode == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-65sdr", "Errors.User.Code.NotFound") 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 { 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 { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-Gns8i", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
repoAuthRequest := model.AuthRequestFromModel(authRequest) repoAuthRequest := model.AuthRequestFromModel(authRequest)
agg := ExternalLoginCheckSucceededAggregate(es.AggregateCreator(), repoUser, repoAuthRequest) 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) { 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) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
user, err := es.UserByID(ctx, userID)
user, err := es.HumanByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-9AuLE", "Errors.User.NotHuman")
}
if user.Password == nil { if user.Password == nil {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Fds3s", "Errors.User.Password.Empty") 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 { func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string, notifyType usr_model.NotificationType) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-33ywz", "Errors.User.NotHuman")
}
if user.State == usr_model.UserStateInitial { if user.State == usr_model.UserStateInitial {
return errors.ThrowPreconditionFailed(nil, "EVENT-Hs11s", "Errors.User.NotInitialised") 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 { func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s09ow", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-tbVAo", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
agg := PasswordCodeSentAggregate(es.AggregateCreator(), repoUser) agg := PasswordCodeSentAggregate(es.AggregateCreator(), repoUser)
@ -812,13 +769,10 @@ func (es *UserEventstore) AddExternalIDP(ctx context.Context, externalIDP *usr_m
if externalIDP == nil || !externalIDP.IsValid() { if externalIDP == nil || !externalIDP.IsValid() {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Ek9s", "Errors.User.ExternalIDP.Invalid") 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 { if err != nil {
return nil, err return nil, err
} }
if existingUser.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Cnk8s", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(existingUser) repoUser := model.UserFromModel(existingUser)
repoExternalIDP := model.ExternalIDPFromModel(externalIDP) repoExternalIDP := model.ExternalIDPFromModel(externalIDP)
aggregates, err := ExternalIDPAddedAggregate(ctx, es.Eventstore.AggregateCreator(), repoUser, repoExternalIDP) 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") 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 { if err != nil {
return err return err
} }
if existingUser.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-Cnk8s", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(existingUser) repoUser := model.UserFromModel(existingUser)
repoExternalIDPs := model.ExternalIDPsFromModel(externalIDPs) repoExternalIDPs := model.ExternalIDPsFromModel(externalIDPs)
aggregates, err := ExternalIDPAddedAggregate(ctx, es.Eventstore.AggregateCreator(), repoUser, repoExternalIDPs...) 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() { if externalIDP == nil || !externalIDP.IsValid() {
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-Cm8sj", "Errors.User.ExternalIDP.Invalid") 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }
if existingUser.Human == nil {
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-E8iod", "Errors.User.NotHuman")
}
_, existingIDP := existingUser.GetExternalIDP(externalIDP) _, existingIDP := existingUser.GetExternalIDP(externalIDP)
if existingIDP == nil { if existingIDP == nil {
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-3Dh7s", "Errors.User.ExternalIDP.NotOnUser") 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) { func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-BaE4M", "Errors.User.NotHuman")
}
if user.Profile != nil { if user.Profile != nil {
return user.Profile, nil return user.Profile, nil
@ -928,13 +870,10 @@ func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model.
if !profile.IsValid() { if !profile.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d82i3", "Errors.User.ProfileInvalid") 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 { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Xhw8Y", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
repoProfile := model.ProfileFromModel(profile) 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) { func (es *UserEventstore) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-zHtOg", "Errors.User.NotHuman")
}
if user.Email != nil { if user.Email != nil {
return 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() { if !email.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "Errors.User.EmailInvalid") 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 { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-tgBdL", "Errors.User.NotHuman")
}
if user.State == usr_model.UserStateInitial { if user.State == usr_model.UserStateInitial {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-3H4q", "Errors.User.NotInitialised") 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 { func (es *UserEventstore) VerifyEmail(ctx context.Context, userID, verificationCode string) error {
if userID == "" { user, err := es.HumanByID(ctx, 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)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil { if verificationCode == "" {
return errors.ThrowPreconditionFailed(nil, "EVENT-YgXu6", "Errors.User.NotHuman") return caos_errs.ThrowPreconditionFailed(nil, "EVENT-skDws", "Errors.User.Code.Empty")
} }
if user.EmailCode == nil { if user.EmailCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-lso9w", "Errors.User.Code.NotFound") 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 { func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userID string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-hqUZP", "Errors.User.NotHuman")
}
if user.State == usr_model.UserStateInitial { if user.State == usr_model.UserStateInitial {
return errors.ThrowPreconditionFailed(nil, "EVENT-E3fbw", "Errors.User.NotInitialised") 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 { func (es *UserEventstore) EmailVerificationCodeSent(ctx context.Context, userID string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-spo0w", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-BcFVd", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
agg := EmailCodeSentAggregate(es.AggregateCreator(), repoUser) 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) { func (es *UserEventstore) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-LwQeA", "Errors.User.NotHuman")
}
if user.Phone != nil { if user.Phone != nil {
return 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() { if !phone.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9s4", "Errors.User.PhoneInvalid") 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 { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-oREkn", "Errors.User.NotHuman")
}
phoneCode, err := phone.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode) phoneCode, err := phone.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
if err != nil { if err != nil {
@ -1156,13 +1059,10 @@ func (es *UserEventstore) VerifyPhone(ctx context.Context, userID, verificationC
if userID == "" || verificationCode == "" { if userID == "" || verificationCode == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dsi8s", "Errors.User.UserIDMissing") 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 { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-UspdK", "Errors.User.NotHuman")
}
if user.PhoneCode == nil { if user.PhoneCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-slp0s", "Errors.User.Code.NotFound") 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 { func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userID string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9sw", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-eEi05", "Errors.User.NotHuman")
}
if user.Phone == nil { if user.Phone == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp9fs", "Errors.User.PhoneNotFound") 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 { func (es *UserEventstore) PhoneVerificationCodeSent(ctx context.Context, userID string) error {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0wa", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-5bhOP", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
agg := PhoneCodeSentAggregate(es.AggregateCreator(), repoUser) 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 { 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 { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-Satfl", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
removeAggregate := PhoneRemovedAggregate(es.AggregateCreator(), repoUser) removeAggregate := PhoneRemovedAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, removeAggregate) 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) { func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) {
if userID == "" { user, err := es.HumanByID(ctx, userID)
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di8ws", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-pHrLu", "Errors.User.NotHuman")
}
if user.Address != nil { if user.Address != nil {
return 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) { 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 { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-crpHD", "Errors.User.NotHuman")
}
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
repoAddress := model.AddressFromModel(address) 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) { 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 { if err != nil {
return nil, err return nil, err
} }
if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-XJvu3", "Errors.User.NotHuman")
}
if user.IsOTPReady() { 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 == "" { if accountName == "" {
accountName = user.UserName 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 { 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 { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-WsBv9", "Errors.User.NotHuman")
}
if user.OTP == nil { 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) repoUser := model.UserFromModel(user)
updateAggregate := MFAOTPRemoveAggregate(es.AggregateCreator(), repoUser) updateAggregate := MFAOTPRemoveAggregate(es.AggregateCreator(), repoUser)
@ -1365,21 +1235,18 @@ func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error {
return nil return nil
} }
func (es *UserEventstore) CheckMfaOTPSetup(ctx context.Context, userID, code string) error { func (es *UserEventstore) CheckMFAOTPSetup(ctx context.Context, userID, code string) error {
user, err := es.UserByID(ctx, userID) user, err := es.HumanByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-7zRQM", "Errors.User.NotHuman")
}
if user.OTP == nil { 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() { 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 return err
} }
repoUser := model.UserFromModel(user) repoUser := model.UserFromModel(user)
@ -1392,23 +1259,20 @@ func (es *UserEventstore) CheckMfaOTPSetup(ctx context.Context, userID, code str
return nil return nil
} }
func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string, authRequest *req_model.AuthRequest) error { func (es *UserEventstore) CheckMFAOTP(ctx context.Context, userID, code string, authRequest *req_model.AuthRequest) error {
user, err := es.UserByID(ctx, userID) user, err := es.HumanByID(ctx, userID)
if err != nil { if err != nil {
return err return err
} }
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-ckqn5", "Errors.User.NotHuman")
}
if !user.IsOTPReady() { 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) repoUser := model.UserFromModel(user)
repoAuthReq := model.AuthRequestFromModel(authRequest) repoAuthReq := model.AuthRequestFromModel(authRequest)
var aggregate func(*es_models.AggregateCreator, *model.User, *model.AuthRequest) es_sdk.AggregateFunc var aggregate func(*es_models.AggregateCreator, *model.User, *model.AuthRequest) es_sdk.AggregateFunc
var checkErr error var checkErr error
if checkErr = es.verifyMfaOTP(user.OTP, code); checkErr != nil { if checkErr = es.verifyMFAOTP(user.OTP, code); checkErr != nil {
aggregate = MFAOTPCheckFailedAggregate aggregate = MFAOTPCheckFailedAggregate
} else { } else {
aggregate = MFAOTPCheckSucceededAggregate aggregate = MFAOTPCheckSucceededAggregate
@ -1425,7 +1289,7 @@ func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string,
return nil 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) decrypt, err := crypto.DecryptString(otp.Secret, es.Multifactors.OTP.CryptoMFA)
if err != nil { if err != nil {
return err return err
@ -1433,11 +1297,222 @@ func (es *UserEventstore) verifyMfaOTP(otp *usr_model.OTP, code string) error {
valid := es.validateTOTP(code, decrypt) valid := es.validateTOTP(code, decrypt)
if !valid { 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 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 { func (es *UserEventstore) SignOut(ctx context.Context, agentID string, userIDs []string) error {
users := make([]*model.User, len(userIDs)) users := make([]*model.User, len(userIDs))
for i, id := range userIDs { for i, id := range userIDs {

View File

@ -442,10 +442,10 @@ func GetMockManipulateUserWithOTP(ctrl *gomock.Controller, decrypt, verified boo
}, },
} }
dataUser, _ := json.Marshal(user) dataUser, _ := json.Marshal(user)
dataOtp, _ := json.Marshal(otp) dataOTP, _ := json.Marshal(otp)
events := []*es_models.Event{ events := []*es_models.Event{
{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: dataUser}, {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 { if verified {
events = append(events, &es_models.Event{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.MFAOTPVerified}) events = append(events, &es_models.Event{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.MFAOTPVerified})

View File

@ -1207,7 +1207,7 @@ func TestInitCodeVerify(t *testing.T) {
} }
} }
func TestSkipMfaInit(t *testing.T) { func TestSkipMFAInit(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
type args struct { type args struct {
es *UserEventstore es *UserEventstore
@ -1256,7 +1256,7 @@ func TestSkipMfaInit(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if tt.res.errFunc == nil && err != nil {
t.Errorf("rshould not get err") 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) ctrl := gomock.NewController(t)
type args struct { type args struct {
es *UserEventstore es *UserEventstore
@ -3578,7 +3578,7 @@ func TestCheckMfaOTPSetup(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if tt.res.errFunc == nil && err != nil {
t.Errorf("result should not get err") 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) ctrl := gomock.NewController(t)
type args struct { type args struct {
es *UserEventstore es *UserEventstore
@ -3708,7 +3708,7 @@ func TestCheckMfaOTP(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if tt.res.errFunc == nil && err != nil {
t.Errorf("result should not get err, got : %v", err) t.Errorf("result should not get err, got : %v", err)

View File

@ -18,12 +18,27 @@ type AuthRequest struct {
} }
func AuthRequestFromModel(request *model.AuthRequest) *AuthRequest { func AuthRequestFromModel(request *model.AuthRequest) *AuthRequest {
return &AuthRequest{ req := &AuthRequest{
ID: request.ID, ID: request.ID,
UserAgentID: request.AgentID, UserAgentID: request.AgentID,
BrowserInfo: BrowserInfoFromModel(request.BrowserInfo),
SelectedIDPConfigID: request.SelectedIDPConfigID, 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 { 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 { func (a *AuthRequest) SetData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, a); err != nil { if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-T5df6").WithError(err).Error("could not unmarshal event data") logging.Log("EVEN-T5df6").WithError(err).Error("could not unmarshal event data")

View File

@ -2,7 +2,6 @@ package model
import ( import (
"encoding/json" "encoding/json"
"github.com/caos/logging" "github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
@ -29,19 +28,19 @@ func OTPToModel(otp *OTP) *model.OTP {
return &model.OTP{ return &model.OTP{
ObjectRoot: otp.ObjectRoot, ObjectRoot: otp.ObjectRoot,
Secret: otp.Secret, Secret: otp.Secret,
State: model.MfaState(otp.State), State: model.MFAState(otp.State),
} }
} }
func (u *Human) appendOTPAddedEvent(event *es_models.Event) error { func (u *Human) appendOTPAddedEvent(event *es_models.Event) error {
u.OTP = &OTP{ u.OTP = &OTP{
State: int32(model.MfaStateNotReady), State: int32(model.MFAStateNotReady),
} }
return u.OTP.setData(event) return u.OTP.setData(event)
} }
func (u *Human) appendOTPVerifiedEvent() { func (u *Human) appendOTPVerifiedEvent() {
u.OTP.State = int32(model.MfaStateReady) u.OTP.State = int32(model.MFAStateReady)
} }
func (u *Human) appendOTPRemovedEvent() { func (u *Human) appendOTPRemovedEvent() {

View File

@ -9,7 +9,7 @@ import (
"github.com/caos/zitadel/internal/user/model" "github.com/caos/zitadel/internal/user/model"
) )
func TestAppendMfaOTPAddedEvent(t *testing.T) { func TestAppendMFAOTPAddedEvent(t *testing.T) {
type args struct { type args struct {
user *Human user *Human
otp *OTP otp *OTP
@ -27,7 +27,7 @@ func TestAppendMfaOTPAddedEvent(t *testing.T) {
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}, otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
event: &es_models.Event{}, 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 { 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 { type args struct {
user *Human user *Human
otp *OTP otp *OTP
@ -62,7 +62,7 @@ func TestAppendMfaOTPVerifyEvent(t *testing.T) {
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}, otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
event: &es_models.Event{}, 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 { 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 { type args struct {
user *Human user *Human
otp *OTP otp *OTP

View File

@ -118,6 +118,22 @@ const (
HumanMFAOTPCheckFailed models.EventType = "user.human.mfa.otp.check.failed" HumanMFAOTPCheckFailed models.EventType = "user.human.mfa.otp.check.failed"
HumanMFAInitSkipped models.EventType = "user.human.mfa.init.skipped" 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" HumanSignedOut models.EventType = "user.human.signed.out"
) )

View File

@ -19,12 +19,16 @@ type Human struct {
*Email *Email
*Phone *Phone
*Address *Address
ExternalIDPs []*ExternalIDP `json:"-"` ExternalIDPs []*ExternalIDP `json:"-"`
InitCode *InitUserCode `json:"-"` InitCode *InitUserCode `json:"-"`
EmailCode *EmailCode `json:"-"` EmailCode *EmailCode `json:"-"`
PhoneCode *PhoneCode `json:"-"` PhoneCode *PhoneCode `json:"-"`
PasswordCode *PasswordCode `json:"-"` PasswordCode *PasswordCode `json:"-"`
OTP *OTP `json:"-"` OTP *OTP `json:"-"`
U2FTokens []*WebAuthNToken `json:"-"`
PasswordlessTokens []*WebAuthNToken `json:"-"`
U2FLogins []*WebAuthNLogin `json:"-"`
PasswordlessLogins []*WebAuthNLogin `json:"-"`
} }
type InitUserCode struct { type InitUserCode struct {
@ -56,6 +60,15 @@ func HumanFromModel(user *model.Human) *Human {
if user.ExternalIDPs != nil { if user.ExternalIDPs != nil {
human.ExternalIDPs = ExternalIDPsFromModel(user.ExternalIDPs) 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 return human
} }
@ -94,6 +107,15 @@ func HumanToModel(user *Human) *model.Human {
if user.OTP != nil { if user.OTP != nil {
human.OTP = OTPToModel(user.OTP) 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 return human
} }
@ -133,10 +155,10 @@ func (h *Human) AppendEvent(event *es_models.Event) (err error) {
HumanAdded, HumanAdded,
HumanRegistered, HumanRegistered,
HumanProfileChanged: HumanProfileChanged:
h.setData(event) err = h.setData(event)
case InitializedUserCodeAdded, case InitializedUserCodeAdded,
InitializedHumanCodeAdded: InitializedHumanCodeAdded:
h.appendInitUsercodeCreatedEvent(event) err = h.appendInitUsercodeCreatedEvent(event)
case UserPasswordChanged, case UserPasswordChanged,
HumanPasswordChanged: HumanPasswordChanged:
err = h.appendUserPasswordChangedEvent(event) err = h.appendUserPasswordChangedEvent(event)
@ -180,6 +202,26 @@ func (h *Human) AppendEvent(event *es_models.Event) (err error) {
err = h.appendExternalIDPAddedEvent(event) err = h.appendExternalIDPAddedEvent(event)
case HumanExternalIDPRemoved, HumanExternalIDPCascadeRemoved: case HumanExternalIDPRemoved, HumanExternalIDPCascadeRemoved:
err = h.appendExternalIDPRemovedEvent(event) 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 { if err != nil {
return err return err

Some files were not shown because too many files have changed in this diff Show More