diff --git a/.schema/openapi/patches/identity.yaml b/.schema/openapi/patches/identity.yaml index a227523488cf..c5f5ede63c58 100644 --- a/.schema/openapi/patches/identity.yaml +++ b/.schema/openapi/patches/identity.yaml @@ -11,6 +11,7 @@ - oidc - webauthn - lookup_secret + - code - op: add path: /paths/~1admin~1identities~1{id}/get/parameters/1/schema/items/enum value: @@ -19,6 +20,7 @@ - oidc - webauthn - lookup_secret + - code - op: remove path: /components/schemas/updateIdentityBody/properties/metadata_admin/type - op: remove @@ -32,4 +34,3 @@ - op: add path: /components/schemas/nullJsonRawMessage/nullable value: true - diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index aba0dde128b0..a966cf27401e 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -17,6 +17,7 @@ - "$ref": "#/components/schemas/updateRegistrationFlowWithPasswordMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithOidcMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" - op: add path: /components/schemas/updateRegistrationFlowBody/discriminator value: @@ -25,6 +26,13 @@ password: "#/components/schemas/updateRegistrationFlowWithPasswordMethod" oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + code: "#/components/schemas/updateRegistrationFlowWithCodeMethod" +- op: add + path: /components/schemas/registrationFlowState/enum + value: + - choose_method + - sent_email + - passed_challenge # end # All modifications for the login flow @@ -38,6 +46,7 @@ - "$ref": "#/components/schemas/updateLoginFlowWithTotpMethod" - "$ref": "#/components/schemas/updateLoginFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" - op: add path: /components/schemas/updateLoginFlowBody/discriminator value: @@ -48,6 +57,13 @@ totp: "#/components/schemas/updateLoginFlowWithTotpMethod" webauthn: "#/components/schemas/updateLoginFlowWithWebAuthnMethod" lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod" + code: "#/components/schemas/updateLoginFlowWithCodeMethod" +- op: add + path: /components/schemas/loginFlowState/enum + value: + - choose_method + - sent_email + - passed_challenge # end # All modifications for the recovery flow diff --git a/Makefile b/Makefile index d1eb8fae5ef1..854832f11cb6 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,13 @@ test-short: .PHONY: test-coverage test-coverage: .bin/go-acc .bin/goveralls - go-acc -o coverage.out ./... -- -v -failfast -timeout=20m -tags sqlite + go-acc -o coverage.out ./... -- -failfast -timeout=20m -tags sqlite,json1 + +.PHONY: test-coverage-next +test-coverage-next: .bin/go-acc .bin/goveralls + go test -short -failfast -timeout=20m -tags sqlite,json1 -cover ./... --args test.gocoverdir="$$PWD/coverage" + go tool covdata percent -i=coverage + go tool covdata textfmt -i=./coverage -o coverage.new.out # Generates the SDK .PHONY: sdk diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go index d3775d1eafb1..afdad6fcd3fe 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -27,8 +27,10 @@ import ( "github.com/ory/x/clidoc" ) -var aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) -var inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) +var ( + aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) + inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) +) var messages map[string]*text.Message @@ -151,6 +153,18 @@ func init() { "NewInfoSelfServiceContinueLoginWebAuthn": text.NewInfoSelfServiceContinueLoginWebAuthn(), "NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(), "NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(), + "NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(), + "NewLoginEmailWithCodeSent": text.NewLoginEmailWithCodeSent(), + "NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed": text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed(), + "NewErrorValidationLoginCodeInvalidOrAlreadyUsed": text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed(), + "NewErrorValidationNoCodeUser": text.NewErrorValidationNoCodeUser(), + "NewInfoNodeLabelRegistrationCode": text.NewInfoNodeLabelRegistrationCode(), + "NewInfoNodeLabelLoginCode": text.NewInfoNodeLabelLoginCode(), + "NewErrorValidationLoginRetrySuccessful": text.NewErrorValidationLoginRetrySuccessful(), + "NewErrorValidationTraitsMismatch": text.NewErrorValidationTraitsMismatch(), + "NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(), + "NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(), + "NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(), } } diff --git a/courier/email_templates.go b/courier/email_templates.go index 8d5d51f0ceaa..d2bae0a197e4 100644 --- a/courier/email_templates.go +++ b/courier/email_templates.go @@ -40,6 +40,8 @@ const ( TypeVerificationCodeValid TemplateType = "verification_code_valid" TypeOTP TemplateType = "otp" TypeTestStub TemplateType = "stub" + TypeLoginCodeValid TemplateType = "login_code_valid" + TypeRegistrationCodeValid TemplateType = "registration_code_valid" ) func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) { @@ -60,6 +62,10 @@ func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) { return TypeVerificationCodeInvalid, nil case *email.VerificationCodeValid: return TypeVerificationCodeValid, nil + case *email.LoginCodeValid: + return TypeLoginCodeValid, nil + case *email.RegistrationCodeValid: + return TypeRegistrationCodeValid, nil case *email.TestStub: return TypeTestStub, nil default: @@ -123,6 +129,18 @@ func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTem return nil, err } return email.NewTestStub(d, &t), nil + case TypeLoginCodeValid: + var t email.LoginCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewLoginCodeValid(d, &t), nil + case TypeRegistrationCodeValid: + var t email.RegistrationCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRegistrationCodeValid(d, &t), nil default: return nil, errors.Errorf("received unexpected message template type: %s", msg.TemplateType) } diff --git a/courier/email_templates_test.go b/courier/email_templates_test.go index 2e8f8520bb7f..40afb5dc6863 100644 --- a/courier/email_templates_test.go +++ b/courier/email_templates_test.go @@ -27,6 +27,8 @@ func TestGetTemplateType(t *testing.T) { courier.TypeVerificationCodeInvalid: &email.VerificationCodeInvalid{}, courier.TypeVerificationCodeValid: &email.VerificationCodeValid{}, courier.TypeTestStub: &email.TestStub{}, + courier.TypeLoginCodeValid: &email.LoginCodeValid{}, + courier.TypeRegistrationCodeValid: &email.RegistrationCodeValid{}, } { t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) { actualType, err := courier.GetEmailTemplateType(tmpl) @@ -50,6 +52,8 @@ func TestNewEmailTemplateFromMessage(t *testing.T) { courier.TypeVerificationCodeInvalid: email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{To: "baz"}), courier.TypeVerificationCodeValid: email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{To: "faz", VerificationURL: "http://bar.foo", VerificationCode: "123456678"}), courier.TypeTestStub: email.NewTestStub(reg, &email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), + courier.TypeLoginCodeValid: email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{To: "far", LoginCode: "123456"}), + courier.TypeRegistrationCodeValid: email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{To: "far", RegistrationCode: "123456"}), } { t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { tmplData, err := json.Marshal(expectedTmpl) @@ -84,7 +88,6 @@ func TestNewEmailTemplateFromMessage(t *testing.T) { actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext(ctx) require.NoError(t, err) require.Equal(t, expectedBodyPlaintext, actualBodyPlaintext) - }) } } diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl new file mode 100644 index 000000000000..505684b9849b --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please login to your account by entering the following code: + +{{ .LoginCode }} diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 000000000000..505684b9849b --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please login to your account by entering the following code: + +{{ .LoginCode }} diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl new file mode 100644 index 000000000000..19d7bfd57d49 --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Login to your account diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl new file mode 100644 index 000000000000..6b9c31799995 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please complete your account registration by entering the following code: + +{{ .RegistrationCode }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 000000000000..6b9c31799995 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please complete your account registration by entering the following code: + +{{ .RegistrationCode }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl new file mode 100644 index 000000000000..0f36292619ef --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Complete your account registration diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go new file mode 100644 index 000000000000..2debc3a0cb7c --- /dev/null +++ b/courier/template/email/login_code_valid.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + LoginCodeValid struct { + deps template.Dependencies + model *LoginCodeValidModel + } + LoginCodeValidModel struct { + To string + LoginCode string + Identity map[string]interface{} + } +) + +func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid { + return &LoginCodeValid{deps: d, model: m} +} + +func (t *LoginCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *LoginCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.subject.gotmpl", "login_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *LoginCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.gotmpl", "login_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.HTML) +} + +func (t *LoginCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.plaintext.gotmpl", "login_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.PlainText) +} + +func (t *LoginCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} diff --git a/courier/template/email/login_code_valid_test.go b/courier/template/email/login_code_valid_test.go new file mode 100644 index 000000000000..dca97defe08c --- /dev/null +++ b/courier/template/email/login_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/internal" +) + +func TestLoginCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/login_code/valid", courier.TypeLoginCodeValid) + }) +} diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go new file mode 100644 index 000000000000..f7e39e334976 --- /dev/null +++ b/courier/template/email/registration_code_valid.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RegistrationCodeValid struct { + deps template.Dependencies + model *RegistrationCodeValidModel + } + RegistrationCodeValidModel struct { + To string + Traits map[string]interface{} + RegistrationCode string + } +) + +func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid { + return &RegistrationCodeValid{deps: d, model: m} +} + +func (t *RegistrationCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *RegistrationCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.subject.gotmpl", "registration_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *RegistrationCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.gotmpl", "registration_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.HTML) +} + +func (t *RegistrationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.plaintext.gotmpl", "registration_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.PlainText) +} + +func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} diff --git a/courier/template/email/registration_code_valid_test.go b/courier/template/email/registration_code_valid_test.go new file mode 100644 index 000000000000..be4cfe8059ea --- /dev/null +++ b/courier/template/email/registration_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/internal" +) + +func TestRegistrationCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/registration_code/valid", courier.TypeRegistrationCodeValid) + }) +} diff --git a/courier/template/template.go b/courier/template/template.go index 3ee99428aa5b..483c40bd2f5e 100644 --- a/courier/template/template.go +++ b/courier/template/template.go @@ -19,6 +19,8 @@ type ( CourierTemplatesVerificationValid() *config.CourierEmailTemplate CourierTemplatesRecoveryInvalid() *config.CourierEmailTemplate CourierTemplatesRecoveryValid() *config.CourierEmailTemplate + CourierTemplatesLoginValid() *config.CourierEmailTemplate + CourierTemplatesRegistrationValid() *config.CourierEmailTemplate } Dependencies interface { diff --git a/courier/template/testhelpers/testhelpers.go b/courier/template/testhelpers/testhelpers.go index 936eedb0a65e..6c2923dbe416 100644 --- a/courier/template/testhelpers/testhelpers.go +++ b/courier/template/testhelpers/testhelpers.go @@ -40,7 +40,8 @@ func SetupRemoteConfig(t *testing.T, ctx context.Context, plaintext string, html func TestRendered(t *testing.T, ctx context.Context, tpl interface { EmailBody(context.Context) (string, error) EmailSubject(context.Context) (string, error) -}) { +}, +) { rendered, err := tpl.EmailBody(ctx) require.NoError(t, err) assert.NotEmpty(t, rendered) @@ -83,6 +84,10 @@ func TestRemoteTemplates(t *testing.T, basePath string, tmplType courier.Templat return email.NewVerificationCodeInvalid(d, &email.VerificationCodeInvalidModel{}) case courier.TypeVerificationCodeValid: return email.NewVerificationCodeValid(d, &email.VerificationCodeValidModel{}) + case courier.TypeLoginCodeValid: + return email.NewLoginCodeValid(d, &email.LoginCodeValidModel{}) + case courier.TypeRegistrationCodeValid: + return email.NewRegistrationCodeValid(d, &email.RegistrationCodeValidModel{}) default: return nil } diff --git a/coverage/.gitignore b/coverage/.gitignore new file mode 100644 index 000000000000..72e8ffc0db8a --- /dev/null +++ b/coverage/.gitignore @@ -0,0 +1 @@ +* diff --git a/driver/config/config.go b/driver/config/config.go index 1a0b76c6eb90..c0614d636894 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -68,6 +68,8 @@ const ( ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email" ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy" ViperKeyCourierHTTPRequestConfig = "courier.http.request_config" + ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email" + ViperKeyCourierTemplatesRegistrationCodeValidEmail = "courier.templates.registration_code.valid.email" ViperKeyCourierSMTPFrom = "courier.smtp.from_address" ViperKeyCourierSMTPFromName = "courier.smtp.from_name" ViperKeyCourierSMTPHeaders = "courier.smtp.headers" @@ -226,6 +228,10 @@ type ( Enabled bool `json:"enabled"` Config json.RawMessage `json:"config"` } + SelfServiceStrategyCode struct { + *SelfServiceStrategy + PasswordlessEnabled bool `json:"passwordless_enabled"` + } Schema struct { ID string `json:"id" koanf:"id"` URL string `json:"url" koanf:"url"` @@ -279,6 +285,8 @@ type ( CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate CourierMessageRetries(ctx context.Context) int } ) @@ -729,7 +737,8 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self config = c } - enabledKey := fmt.Sprintf("%s.%s.enabled", ViperKeySelfServiceStrategyConfig, strategy) + basePath := fmt.Sprintf("%s.%s", ViperKeySelfServiceStrategyConfig, strategy) + enabledKey := fmt.Sprintf("%s.enabled", basePath) s := &SelfServiceStrategy{ Enabled: pp.Bool(enabledKey), Config: json.RawMessage(config), @@ -739,6 +748,7 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self // we need to forcibly set these values here: if !pp.Exists(enabledKey) { switch strategy { + case "otp": case "password": fallthrough case "profile": @@ -755,6 +765,37 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self return s } +func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrategyCode { + pp := p.GetProvider(ctx) + + config := "{}" + out, err := pp.Marshal(kjson.Parser()) + if err != nil { + p.l.WithError(err).Warn("Unable to marshal self service strategy configuration.") + } else if c := gjson.GetBytes(out, + fmt.Sprintf("%s.%s.config", ViperKeySelfServiceStrategyConfig, "code")).Raw; len(c) > 0 { + config = c + } + + basePath := fmt.Sprintf("%s.%s", ViperKeySelfServiceStrategyConfig, "code") + enabledKey := fmt.Sprintf("%s.enabled", basePath) + passwordlessKey := fmt.Sprintf("%s.passwordless_enabled", basePath) + + s := &SelfServiceStrategyCode{ + SelfServiceStrategy: &SelfServiceStrategy{ + Enabled: pp.Bool(enabledKey), + Config: json.RawMessage(config), + }, + PasswordlessEnabled: pp.Bool(passwordlessKey), + } + + if !pp.Exists(enabledKey) { + s.PasswordlessEnabled = false + s.Enabled = true + } + return s +} + func (p *Config) SecretsDefault(ctx context.Context) [][]byte { pp := p.GetProvider(ctx) secrets := pp.Strings(ViperKeySecretsDefault) @@ -1096,6 +1137,14 @@ func (p *Config) CourierTemplatesVerificationCodeValid(ctx context.Context) *Cou return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidEmail) } +func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail) +} + +func (p *Config) CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidEmail) +} + func (p *Config) CourierMessageRetries(ctx context.Context) int { return p.GetProvider(ctx).IntF(ViperKeyCourierMessageRetries, 5) } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 365b19323fd7..dee0baa5e657 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -381,8 +381,10 @@ func TestViperProvider(t *testing.T) { t.Run("group=hashers", func(t *testing.T) { c := p.HasherArgon2(ctx) - assert.Equal(t, &config.Argon2{Memory: 1048576, Iterations: 2, Parallelism: 4, - SaltLength: 16, KeyLength: 32, DedicatedMemory: config.Argon2DefaultDedicatedMemory, ExpectedDeviation: config.Argon2DefaultDeviation, ExpectedDuration: config.Argon2DefaultDuration}, c) + assert.Equal(t, &config.Argon2{ + Memory: 1048576, Iterations: 2, Parallelism: 4, + SaltLength: 16, KeyLength: 32, DedicatedMemory: config.Argon2DefaultDedicatedMemory, ExpectedDeviation: config.Argon2DefaultDeviation, ExpectedDuration: config.Argon2DefaultDuration, + }, c) }) t.Run("group=set_provider_by_json", func(t *testing.T) { @@ -505,6 +507,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) }, }, @@ -520,6 +523,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) }, }, @@ -535,6 +539,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) }, }, { @@ -561,7 +566,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) - + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) assert.False(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)) assert.False(t, p.SelfServiceFlowVerificationNotifyUnknownRecipients(ctx)) }) @@ -897,7 +902,6 @@ func TestLoadingTLSConfig(t *testing.T) { assert.Equal(t, "Unable to load HTTPS TLS Certificate", hook.LastEntry().Message) assert.True(t, *exited) }) - } func TestIdentitySchemaValidation(t *testing.T) { @@ -1022,7 +1026,6 @@ func TestIdentitySchemaValidation(t *testing.T) { assert.Error(t, e) assert.Contains(t, e.Error(), "Client.Timeout") } - }) t.Run("case=validate schema is validated on file change", func(t *testing.T) { @@ -1051,7 +1054,7 @@ func TestIdentitySchemaValidation(t *testing.T) { // There are a bunch of log messages beeing logged. We are looking for a specific one. timeout := time.After(time.Millisecond * 500) - var success = false + success := false for !success { for _, v := range hook.AllEntries() { s, err := v.String() @@ -1064,7 +1067,7 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Fatal("the test could not complete as the context timed out before the file watcher updated") case <-timeout: t.Fatal("Expected log line was not encountered within specified timeout") - default: //nothing + default: // nothing } } diff --git a/driver/registry_default.go b/driver/registry_default.go index d0ffa2d2c198..514409f5bd1e 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -97,11 +97,12 @@ type RegistryDefault struct { persister persistence.Persister migrationStatus popx.MigrationStatuses - hookVerifier *hook.Verifier - hookSessionIssuer *hook.SessionIssuer - hookSessionDestroyer *hook.SessionDestroyer - hookAddressVerifier *hook.AddressVerifier - hookShowVerificationUI *hook.ShowVerificationUIHook + hookVerifier *hook.Verifier + hookSessionIssuer *hook.SessionIssuer + hookSessionDestroyer *hook.SessionDestroyer + hookAddressVerifier *hook.AddressVerifier + hookShowVerificationUI *hook.ShowVerificationUIHook + hookCodeAddressVerifier *hook.CodeAddressVerifier identityHandler *identity.Handler identityValidator *identity.Validator @@ -327,10 +328,28 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} { return m.selfserviceStrategies } +func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool { + switch id { + case identity.CredentialsTypeCodeAuth.String(): + return m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled + default: + return m.Config().SelfServiceStrategy(ctx, id).Enabled + } +} + +func (m *RegistryDefault) strategyLoginEnabled(ctx context.Context, id string) bool { + switch id { + case identity.CredentialsTypeCodeAuth.String(): + return m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled + default: + return m.Config().SelfServiceStrategy(ctx, id).Enabled + } +} + func (m *RegistryDefault) RegistrationStrategies(ctx context.Context) (registrationStrategies registration.Strategies) { for _, strategy := range m.selfServiceStrategies() { if s, ok := strategy.(registration.Strategy); ok { - if m.Config().SelfServiceStrategy(ctx, string(s.ID())).Enabled { + if m.strategyRegistrationEnabled(ctx, s.ID().String()) { registrationStrategies = append(registrationStrategies, s) } } @@ -352,7 +371,7 @@ func (m *RegistryDefault) AllRegistrationStrategies() registration.Strategies { func (m *RegistryDefault) LoginStrategies(ctx context.Context) (loginStrategies login.Strategies) { for _, strategy := range m.selfServiceStrategies() { if s, ok := strategy.(login.Strategy); ok { - if m.Config().SelfServiceStrategy(ctx, string(s.ID())).Enabled { + if m.strategyLoginEnabled(ctx, s.ID().String()) { loginStrategies = append(loginStrategies, s) } } @@ -660,7 +679,6 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize m.persister = p.WithNetworkID(net.ID) return nil }, bc) - if err != nil { return err } @@ -744,6 +762,14 @@ func (m *RegistryDefault) VerificationCodePersister() code.VerificationCodePersi return m.Persister() } +func (m *RegistryDefault) RegistrationCodePersister() code.RegistrationCodePersister { + return m.Persister() +} + +func (m *RegistryDefault) LoginCodePersister() code.LoginCodePersister { + return m.Persister() +} + func (m *RegistryDefault) Persister() persistence.Persister { return m.persister } diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index 6efffc05a777..c3f809d2144e 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -15,6 +15,13 @@ func (m *RegistryDefault) HookVerifier() *hook.Verifier { return m.hookVerifier } +func (m *RegistryDefault) HookCodeAddressVerifier() *hook.CodeAddressVerifier { + if m.hookCodeAddressVerifier == nil { + m.hookCodeAddressVerifier = hook.NewCodeAddressVerifier(m) + } + return m.hookCodeAddressVerifier +} + func (m *RegistryDefault) HookSessionIssuer() *hook.SessionIssuer { if m.hookSessionIssuer == nil { m.hookSessionIssuer = hook.NewSessionIssuer(m) diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 7f78517891f0..89ed5e656c74 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -12,6 +12,10 @@ import ( ) func (m *RegistryDefault) PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) (b []registration.PostHookPrePersistExecutor) { + if credentialsType == identity.CredentialsTypeCodeAuth && m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled { + b = append(b, m.HookCodeAddressVerifier()) + } + for _, v := range m.getHooks(string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType))) { if hook, ok := v.(registration.PostHookPrePersistExecutor); ok { b = append(b, hook) @@ -35,7 +39,7 @@ func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context, } if len(b) == initialHookCount { - // since we don't want merging hooks defined in a specific strategy and global hooks + // since we don't want merging hooks defined in a specific strategy and // global hooks are added only if no strategy specific hooks are defined for _, v := range m.getHooks(config.HookGlobal, m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, config.HookGlobal)) { if hook, ok := v.(registration.PostHookPostPersistExecutor); ok { diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 3bccf3e24b03..533cdd621a9a 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -627,7 +627,8 @@ func TestDriverDefault_Strategies(t *testing.T) { { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - }}, + }, + }, { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) @@ -649,6 +650,13 @@ func TestDriverDefault_Strategies(t *testing.T) { }, expect: []string{"password", "oidc"}, }, + { + prep: func(conf *config.Config) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + }, + expect: []string{"password", "code"}, + }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { conf, reg := internal.NewVeryFastRegistryWithoutDB(t) @@ -672,7 +680,8 @@ func TestDriverDefault_Strategies(t *testing.T) { { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - }}, + }, + }, { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) @@ -694,6 +703,13 @@ func TestDriverDefault_Strategies(t *testing.T) { }, expect: []string{"password", "oidc", "totp"}, }, + { + prep: func(conf *config.Config) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + }, + expect: []string{"password", "code"}, + }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { conf, reg := internal.NewVeryFastRegistryWithoutDB(t) @@ -760,7 +776,8 @@ func TestDriverDefault_Strategies(t *testing.T) { }), configx.SkipValidation()) return c - }}, + }, + }, { prep: func(t *testing.T) *config.Config { c := config.MustNew(t, l, @@ -834,7 +851,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("case=all login strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "totp", "webauthn", "lookup_secret"} + expects := []string{"password", "oidc", "code", "totp", "webauthn", "lookup_secret"} s := reg.AllLoginStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -843,7 +860,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { }) t.Run("case=all registration strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "webauthn"} + expects := []string{"password", "oidc", "code", "webauthn"} s := reg.AllRegistrationStrategies() require.Len(t, s, len(expects)) for k, e := range expects { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 61ba37c6708f..be8dddc87f9b 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -875,6 +875,9 @@ "oidc": { "$ref": "#/definitions/selfServiceAfterOIDCLoginMethod" }, + "code": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, "hooks": { "type": "array", "items": { @@ -947,6 +950,9 @@ "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "code": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "hooks": { "$ref": "#/definitions/selfServiceHooks" } @@ -1447,6 +1453,11 @@ "type": "object", "additionalProperties": false, "properties": { + "passwordless_enabled": { + "type": "boolean", + "title": "Enables Login and Registration with the Code Method", + "default": false + }, "enabled": { "type": "boolean", "title": "Enables Code Method", @@ -1643,8 +1654,12 @@ "display_name" ], "properties": { - "origin": {"not": {}}, - "origins": {"not": {}} + "origin": { + "not": {} + }, + "origins": { + "not": {} + } } }, { @@ -1654,8 +1669,12 @@ "origin" ], "properties": { - "origin": {"type": "string"}, - "origins": {"not": {}} + "origin": { + "type": "string" + }, + "origins": { + "not": {} + } } }, { @@ -1665,8 +1684,15 @@ "origins" ], "properties": { - "origin": {"not": {}}, - "origins": {"type": "array", "items": {"type": "string"}} + "origin": { + "not": {} + }, + "origins": { + "type": "array", + "items": { + "type": "string" + } + } } } ] @@ -1805,6 +1831,42 @@ }, "verification_code": { "$ref": "#/definitions/courierTemplates" + }, + "registration_code": { + "additionalProperties": false, + "type": "object", + "properties": { + "valid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + } + }, + "required": [ + "email" + ] + } + } + }, + "login_code": { + "additionalProperties": false, + "type": "object", + "properties": { + "valid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + } + }, + "required": [ + "email" + ] + } + } } } }, @@ -1829,7 +1891,10 @@ "title": "Delivery Strategy", "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", "type": "string", - "enum": ["smtp", "http"], + "enum": [ + "smtp", + "http" + ], "default": "smtp" }, "http": { @@ -2026,10 +2091,10 @@ ] }, "override_return_to": { - "title":"Persist OAuth2 request between flows", - "type":"boolean", - "default":false, - "description":"Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow." + "title": "Persist OAuth2 request between flows", + "type": "boolean", + "default": false, + "description": "Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow." } }, "additionalProperties": false diff --git a/embedx/identity_extension.schema.json b/embedx/identity_extension.schema.json index ef402cdadcfb..9af2e97f07ce 100644 --- a/embedx/identity_extension.schema.json +++ b/embedx/identity_extension.schema.json @@ -38,6 +38,19 @@ "type": "boolean" } } + }, + "code": { + "type": "object", + "additionalProperties": false, + "properties": { + "identifier": { + "type": "boolean" + }, + "via": { + "type": "string", + "enum": ["email"] + } + } } } }, @@ -47,10 +60,7 @@ "properties": { "via": { "type": "string", - "enum": [ - "email", - "phone" - ] + "enum": ["email", "phone"] } } }, @@ -60,9 +70,7 @@ "properties": { "via": { "type": "string", - "enum": [ - "email" - ] + "enum": ["email"] } } } diff --git a/identity/credentials.go b/identity/credentials.go index af79ba3f2ed6..6ccbe867c89a 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -94,6 +94,8 @@ func (c CredentialsType) ToUiNodeGroup() node.UiNodeGroup { return node.WebAuthnGroup case CredentialsTypeLookup: return node.LookupGroup + case CredentialsTypeCodeAuth: + return node.CodeGroup default: return node.DefaultGroup } @@ -106,6 +108,7 @@ const ( CredentialsTypeTOTP CredentialsType = "totp" CredentialsTypeLookup CredentialsType = "lookup_secret" CredentialsTypeWebAuthn CredentialsType = "webauthn" + CredentialsTypeCodeAuth CredentialsType = "code" ) var AllCredentialTypes = []CredentialsType{ @@ -114,6 +117,7 @@ var AllCredentialTypes = []CredentialsType{ CredentialsTypeTOTP, CredentialsTypeLookup, CredentialsTypeWebAuthn, + CredentialsTypeCodeAuth, } const ( @@ -131,6 +135,7 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { CredentialsTypeTOTP, CredentialsTypeLookup, CredentialsTypeWebAuthn, + CredentialsTypeCodeAuth, CredentialsTypeRecoveryLink, CredentialsTypeRecoveryCode, } { @@ -141,6 +146,15 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { return "", false } +// swagger:ignore +type CredentialsIdentifierAddressType string + +const ( + CredentialsIdentifierAddressTypeEmail CredentialsIdentifierAddressType = AddressTypeEmail + CredentialsIdentifierAddressTypePhone CredentialsIdentifierAddressType = AddressTypePhone + CredentialsIdentifierAddressTypeNone CredentialsIdentifierAddressType = "none" +) + // Credentials represents a specific credential type // // swagger:model identityCredentials diff --git a/identity/credentials_code.go b/identity/credentials_code.go new file mode 100644 index 000000000000..184479ae1700 --- /dev/null +++ b/identity/credentials_code.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "database/sql" +) + +type CodeAddressType string + +const ( + CodeAddressTypeEmail CodeAddressType = AddressTypeEmail + CodeAddressTypePhone CodeAddressType = AddressTypePhone +) + +// CredentialsCode represents a one time login/registration code +// +// swagger:model identityCredentialsCode +type CredentialsCode struct { + // The type of the address for this code + AddressType CodeAddressType `json:"address_type"` + + // UsedAt indicates whether and when a recovery code was used. + UsedAt sql.NullTime `json:"used_at,omitempty"` +} diff --git a/identity/extension_credentials.go b/identity/extension_credentials.go index 95a1ee8d4c93..69fb53810fbc 100644 --- a/identity/extension_credentials.go +++ b/identity/extension_credentials.go @@ -11,6 +11,7 @@ import ( "github.com/ory/jsonschema/v3" "github.com/ory/x/sqlxx" "github.com/ory/x/stringslice" + "github.com/ory/x/stringsx" "github.com/ory/kratos/schema" ) @@ -25,7 +26,7 @@ func NewSchemaExtensionCredentials(i *Identity) *SchemaExtensionCredentials { return &SchemaExtensionCredentials{i: i} } -func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}) { +func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}, addressType CredentialsIdentifierAddressType) { cred, ok := r.i.GetCredentials(ct) if !ok { cred = &Credentials{ @@ -43,16 +44,35 @@ func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value int r.i.SetCredentials(ct, *cred) } -func (r *SchemaExtensionCredentials) Run(_ jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { +func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { r.l.Lock() defer r.l.Unlock() if s.Credentials.Password.Identifier { - r.setIdentifier(CredentialsTypePassword, value) + r.setIdentifier(CredentialsTypePassword, value, CredentialsIdentifierAddressTypeNone) } if s.Credentials.WebAuthn.Identifier { - r.setIdentifier(CredentialsTypeWebAuthn, value) + r.setIdentifier(CredentialsTypeWebAuthn, value, CredentialsIdentifierAddressTypeNone) + } + + if s.Credentials.Code.Identifier { + switch f := stringsx.SwitchExact(s.Credentials.Code.Via); { + case f.AddCase(AddressTypeEmail): + if !jsonschema.Formats["email"](value) { + return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) + } + + r.setIdentifier(CredentialsTypeCodeAuth, value, AddressTypeEmail) + // case f.AddCase(AddressTypePhone): + // if !jsonschema.Formats["tel"](value) { + // return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) + // } + // + // r.setIdentifier(CredentialsTypeCodeAuth, value, CredentialsIdentifierAddressType(AddressTypePhone)) + default: + return ctx.Error("", "credentials.code.via has unknown value %q", s.Credentials.Code.Via) + } } return nil diff --git a/identity/extension_credentials_test.go b/identity/extension_credentials_test.go index cf580c6a0c10..95cd9d000c6a 100644 --- a/identity/extension_credentials_test.go +++ b/identity/extension_credentials_test.go @@ -72,6 +72,21 @@ func TestSchemaExtensionCredentials(t *testing.T) { }, ct: identity.CredentialsTypeWebAuthn, }, + { + doc: `{"email":"foo@ory.sh"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expect: []string{"foo@ory.sh"}, + ct: identity.CredentialsTypeCodeAuth, + }, + { + doc: `{"email":"FOO@ory.sh"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expect: []string{"foo@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"not-foo@ory.sh"}, + }, + ct: identity.CredentialsTypeCodeAuth, + }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { c := jsonschema.NewCompiler() diff --git a/identity/stub/extension/credentials/code.schema.json b/identity/stub/extension/credentials/code.schema.json new file mode 100644 index 000000000000..bef244bc9ae5 --- /dev/null +++ b/identity/stub/extension/credentials/code.schema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } +} diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index f7968b85c90e..959499576b3d 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -34,6 +34,7 @@ docs/HealthStatus.md docs/Identity.md docs/IdentityApi.md docs/IdentityCredentials.md +docs/IdentityCredentialsCode.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -52,6 +53,7 @@ docs/IsAlive200Response.md docs/IsReady503Response.md docs/JsonPatch.md docs/LoginFlow.md +docs/LoginFlowState.md docs/LogoutFlow.md docs/Message.md docs/MessageDispatch.md @@ -69,6 +71,7 @@ docs/RecoveryFlowState.md docs/RecoveryIdentityAddress.md docs/RecoveryLinkForIdentity.md docs/RegistrationFlow.md +docs/RegistrationFlowState.md docs/SelfServiceFlowExpiredError.md docs/Session.md docs/SessionAuthenticationMethod.md @@ -92,6 +95,7 @@ docs/UiNodeTextAttributes.md docs/UiText.md docs/UpdateIdentityBody.md docs/UpdateLoginFlowBody.md +docs/UpdateLoginFlowWithCodeMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md docs/UpdateLoginFlowWithPasswordMethod.md @@ -101,6 +105,7 @@ docs/UpdateRecoveryFlowBody.md docs/UpdateRecoveryFlowWithCodeMethod.md docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md +docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md @@ -144,6 +149,7 @@ model_health_not_ready_status.go model_health_status.go model_identity.go model_identity_credentials.go +model_identity_credentials_code.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go model_identity_credentials_password.go @@ -162,6 +168,7 @@ model_is_alive_200_response.go model_is_ready_503_response.go model_json_patch.go model_login_flow.go +model_login_flow_state.go model_logout_flow.go model_message.go model_message_dispatch.go @@ -178,6 +185,7 @@ model_recovery_flow_state.go model_recovery_identity_address.go model_recovery_link_for_identity.go model_registration_flow.go +model_registration_flow_state.go model_self_service_flow_expired_error.go model_session.go model_session_authentication_method.go @@ -201,6 +209,7 @@ model_ui_node_text_attributes.go model_ui_text.go model_update_identity_body.go model_update_login_flow_body.go +model_update_login_flow_with_code_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go model_update_login_flow_with_password_method.go @@ -210,6 +219,7 @@ model_update_recovery_flow_body.go model_update_recovery_flow_with_code_method.go model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go +model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go model_update_registration_flow_with_password_method.go model_update_registration_flow_with_web_authn_method.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index cb48b260e91f..084b578785ee 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -159,6 +159,7 @@ Class | Method | HTTP request | Description - [HealthStatus](docs/HealthStatus.md) - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) + - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) @@ -177,6 +178,7 @@ Class | Method | HTTP request | Description - [IsReady503Response](docs/IsReady503Response.md) - [JsonPatch](docs/JsonPatch.md) - [LoginFlow](docs/LoginFlow.md) + - [LoginFlowState](docs/LoginFlowState.md) - [LogoutFlow](docs/LogoutFlow.md) - [Message](docs/Message.md) - [MessageDispatch](docs/MessageDispatch.md) @@ -193,6 +195,7 @@ Class | Method | HTTP request | Description - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) - [RegistrationFlow](docs/RegistrationFlow.md) + - [RegistrationFlowState](docs/RegistrationFlowState.md) - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) - [Session](docs/Session.md) - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) @@ -216,6 +219,7 @@ Class | Method | HTTP request | Description - [UiText](docs/UiText.md) - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) + - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) @@ -225,6 +229,7 @@ Class | Method | HTTP request | Description - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) + - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) diff --git a/internal/client-go/model_identity_credentials_code.go b/internal/client-go/model_identity_credentials_code.go new file mode 100644 index 000000000000..f542b359639a --- /dev/null +++ b/internal/client-go/model_identity_credentials_code.go @@ -0,0 +1,162 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "time" +) + +// IdentityCredentialsCode CredentialsCode represents a one time login/registration code +type IdentityCredentialsCode struct { + AddressType *string `json:"address_type,omitempty"` + UsedAt NullableTime `json:"used_at,omitempty"` +} + +// NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsCode() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// NewIdentityCredentialsCodeWithDefaults instantiates a new IdentityCredentialsCode object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// GetAddressType returns the AddressType field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddressType() string { + if o == nil || o.AddressType == nil { + var ret string + return ret + } + return *o.AddressType +} + +// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { + if o == nil || o.AddressType == nil { + return nil, false + } + return o.AddressType, true +} + +// HasAddressType returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddressType() bool { + if o != nil && o.AddressType != nil { + return true + } + + return false +} + +// SetAddressType gets a reference to the given string and assigns it to the AddressType field. +func (o *IdentityCredentialsCode) SetAddressType(v string) { + o.AddressType = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityCredentialsCode) GetUsedAt() time.Time { + if o == nil || o.UsedAt.Get() == nil { + var ret time.Time + return ret + } + return *o.UsedAt.Get() +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return o.UsedAt.Get(), o.UsedAt.IsSet() +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasUsedAt() bool { + if o != nil && o.UsedAt.IsSet() { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. +func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { + o.UsedAt.Set(&v) +} + +// SetUsedAtNil sets the value for UsedAt to be an explicit nil +func (o *IdentityCredentialsCode) SetUsedAtNil() { + o.UsedAt.Set(nil) +} + +// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil +func (o *IdentityCredentialsCode) UnsetUsedAt() { + o.UsedAt.Unset() +} + +func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.AddressType != nil { + toSerialize["address_type"] = o.AddressType + } + if o.UsedAt.IsSet() { + toSerialize["used_at"] = o.UsedAt.Get() + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsCode struct { + value *IdentityCredentialsCode + isSet bool +} + +func (v NullableIdentityCredentialsCode) Get() *IdentityCredentialsCode { + return v.value +} + +func (v *NullableIdentityCredentialsCode) Set(val *IdentityCredentialsCode) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCode) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCode(val *IdentityCredentialsCode) *NullableIdentityCredentialsCode { + return &NullableIdentityCredentialsCode{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_identity_credentials_otp.go b/internal/client-go/model_identity_credentials_otp.go new file mode 100644 index 000000000000..b60601987e67 --- /dev/null +++ b/internal/client-go/model_identity_credentials_otp.go @@ -0,0 +1,162 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "time" +) + +// IdentityCredentialsOTP CredentialsOTP represents an OTP code +type IdentityCredentialsOTP struct { + AddressType *string `json:"address_type,omitempty"` + UsedAt NullableTime `json:"used_at,omitempty"` +} + +// NewIdentityCredentialsOTP instantiates a new IdentityCredentialsOTP object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsOTP() *IdentityCredentialsOTP { + this := IdentityCredentialsOTP{} + return &this +} + +// NewIdentityCredentialsOTPWithDefaults instantiates a new IdentityCredentialsOTP object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsOTPWithDefaults() *IdentityCredentialsOTP { + this := IdentityCredentialsOTP{} + return &this +} + +// GetAddressType returns the AddressType field value if set, zero value otherwise. +func (o *IdentityCredentialsOTP) GetAddressType() string { + if o == nil || o.AddressType == nil { + var ret string + return ret + } + return *o.AddressType +} + +// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOTP) GetAddressTypeOk() (*string, bool) { + if o == nil || o.AddressType == nil { + return nil, false + } + return o.AddressType, true +} + +// HasAddressType returns a boolean if a field has been set. +func (o *IdentityCredentialsOTP) HasAddressType() bool { + if o != nil && o.AddressType != nil { + return true + } + + return false +} + +// SetAddressType gets a reference to the given string and assigns it to the AddressType field. +func (o *IdentityCredentialsOTP) SetAddressType(v string) { + o.AddressType = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityCredentialsOTP) GetUsedAt() time.Time { + if o == nil || o.UsedAt.Get() == nil { + var ret time.Time + return ret + } + return *o.UsedAt.Get() +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityCredentialsOTP) GetUsedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return o.UsedAt.Get(), o.UsedAt.IsSet() +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsOTP) HasUsedAt() bool { + if o != nil && o.UsedAt.IsSet() { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. +func (o *IdentityCredentialsOTP) SetUsedAt(v time.Time) { + o.UsedAt.Set(&v) +} + +// SetUsedAtNil sets the value for UsedAt to be an explicit nil +func (o *IdentityCredentialsOTP) SetUsedAtNil() { + o.UsedAt.Set(nil) +} + +// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil +func (o *IdentityCredentialsOTP) UnsetUsedAt() { + o.UsedAt.Unset() +} + +func (o IdentityCredentialsOTP) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.AddressType != nil { + toSerialize["address_type"] = o.AddressType + } + if o.UsedAt.IsSet() { + toSerialize["used_at"] = o.UsedAt.Get() + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsOTP struct { + value *IdentityCredentialsOTP + isSet bool +} + +func (v NullableIdentityCredentialsOTP) Get() *IdentityCredentialsOTP { + return v.value +} + +func (v *NullableIdentityCredentialsOTP) Set(val *IdentityCredentialsOTP) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsOTP) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsOTP) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsOTP(val *IdentityCredentialsOTP) *NullableIdentityCredentialsOTP { + return &NullableIdentityCredentialsOTP{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsOTP) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsOTP) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_identity_credentials_type.go b/internal/client-go/model_identity_credentials_type.go index f02f3f19b898..a9d01c54e796 100644 --- a/internal/client-go/model_identity_credentials_type.go +++ b/internal/client-go/model_identity_credentials_type.go @@ -26,6 +26,7 @@ const ( IDENTITYCREDENTIALSTYPE_OIDC IdentityCredentialsType = "oidc" IDENTITYCREDENTIALSTYPE_WEBAUTHN IdentityCredentialsType = "webauthn" IDENTITYCREDENTIALSTYPE_LOOKUP_SECRET IdentityCredentialsType = "lookup_secret" + IDENTITYCREDENTIALSTYPE_CODE IdentityCredentialsType = "code" ) func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { @@ -35,7 +36,7 @@ func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { return err } enumTypeValue := IdentityCredentialsType(value) - for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret"} { + for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret", "code"} { if existing == enumTypeValue { *v = enumTypeValue return nil diff --git a/internal/client-go/model_login_flow.go b/internal/client-go/model_login_flow.go index 1b3f4b6c7dde..dc7c67ea2649 100644 --- a/internal/client-go/model_login_flow.go +++ b/internal/client-go/model_login_flow.go @@ -39,6 +39,8 @@ type LoginFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the login flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -50,12 +52,13 @@ type LoginFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *LoginFlow { +func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *LoginFlow { this := LoginFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -421,6 +424,32 @@ func (o *LoginFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *LoginFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *LoginFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *LoginFlow) SetState(v interface{}) { + o.State = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -539,6 +568,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_login_flow_state.go b/internal/client-go/model_login_flow_state.go new file mode 100644 index 000000000000..ce5570b79032 --- /dev/null +++ b/internal/client-go/model_login_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// LoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +type LoginFlowState string + +// List of loginFlowState +const ( + LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" + LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" + LOGINFLOWSTATE_PASSED_CHALLENGE LoginFlowState = "passed_challenge" +) + +func (v *LoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := LoginFlowState(value) + for _, existing := range []LoginFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid LoginFlowState", value) +} + +// Ptr returns reference to loginFlowState value +func (v LoginFlowState) Ptr() *LoginFlowState { + return &v +} + +type NullableLoginFlowState struct { + value *LoginFlowState + isSet bool +} + +func (v NullableLoginFlowState) Get() *LoginFlowState { + return v.value +} + +func (v *NullableLoginFlowState) Set(val *LoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowState(val *LoginFlowState) *NullableLoginFlowState { + return &NullableLoginFlowState{value: val, isSet: true} +} + +func (v NullableLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_message.go b/internal/client-go/model_message.go index 3a6f3f92362b..f0452185f169 100644 --- a/internal/client-go/model_message.go +++ b/internal/client-go/model_message.go @@ -28,7 +28,7 @@ type Message struct { SendCount int64 `json:"send_count"` Status CourierMessageStatus `json:"status"` Subject string `json:"subject"` - // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid TemplateType string `json:"template_type"` Type CourierMessageType `json:"type"` // UpdatedAt is a helper struct field for gobuffalo.pop. diff --git a/internal/client-go/model_recovery_flow.go b/internal/client-go/model_recovery_flow.go index 6ae19ebd60e6..acf4ff667df3 100644 --- a/internal/client-go/model_recovery_flow.go +++ b/internal/client-go/model_recovery_flow.go @@ -29,8 +29,9 @@ type RecoveryFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State RecoveryFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type RecoveryFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state RecoveryFlowState, type_ string, ui UiContainer) *RecoveryFlow { +func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RecoveryFlow { this := RecoveryFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -221,9 +222,10 @@ func (o *RecoveryFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *RecoveryFlow) GetState() RecoveryFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RecoveryFlow) GetState() interface{} { if o == nil { - var ret RecoveryFlowState + var ret interface{} return ret } @@ -232,15 +234,16 @@ func (o *RecoveryFlow) GetState() RecoveryFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *RecoveryFlow) GetStateOk() (*RecoveryFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RecoveryFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *RecoveryFlow) SetState(v RecoveryFlowState) { +func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } @@ -312,7 +315,7 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/client-go/model_registration_flow.go b/internal/client-go/model_registration_flow.go index fe9f697b5551..9b08288d6a16 100644 --- a/internal/client-go/model_registration_flow.go +++ b/internal/client-go/model_registration_flow.go @@ -34,6 +34,8 @@ type RegistrationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. + State interface{} `json:"state"` // TransientPayload is used to pass data from the registration to a webhook TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. @@ -45,12 +47,13 @@ type RegistrationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *RegistrationFlow { +func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RegistrationFlow { this := RegistrationFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -320,6 +323,32 @@ func (o *RegistrationFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RegistrationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RegistrationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *RegistrationFlow) SetState(v interface{}) { + o.State = v +} + // GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. func (o *RegistrationFlow) GetTransientPayload() map[string]interface{} { if o == nil || o.TransientPayload == nil { @@ -429,6 +458,9 @@ func (o RegistrationFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if o.TransientPayload != nil { toSerialize["transient_payload"] = o.TransientPayload } diff --git a/internal/client-go/model_registration_flow_state.go b/internal/client-go/model_registration_flow_state.go new file mode 100644 index 000000000000..86f3fd38cff0 --- /dev/null +++ b/internal/client-go/model_registration_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// RegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +type RegistrationFlowState string + +// List of registrationFlowState +const ( + REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" + REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" + REGISTRATIONFLOWSTATE_PASSED_CHALLENGE RegistrationFlowState = "passed_challenge" +) + +func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RegistrationFlowState(value) + for _, existing := range []RegistrationFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) +} + +// Ptr returns reference to registrationFlowState value +func (v RegistrationFlowState) Ptr() *RegistrationFlowState { + return &v +} + +type NullableRegistrationFlowState struct { + value *RegistrationFlowState + isSet bool +} + +func (v NullableRegistrationFlowState) Get() *RegistrationFlowState { + return v.value +} + +func (v *NullableRegistrationFlowState) Set(val *RegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlowState(val *RegistrationFlowState) *NullableRegistrationFlowState { + return &NullableRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_self_service_login_flow_state.go b/internal/client-go/model_self_service_login_flow_state.go new file mode 100644 index 000000000000..093d300fe207 --- /dev/null +++ b/internal/client-go/model_self_service_login_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// SelfServiceLoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +type SelfServiceLoginFlowState string + +// List of SelfServiceLoginFlowState +const ( + SELFSERVICELOGINFLOWSTATE_CHOOSE_METHOD SelfServiceLoginFlowState = "choose_method" + SELFSERVICELOGINFLOWSTATE_SENT_EMAIL SelfServiceLoginFlowState = "sent_email" + SELFSERVICELOGINFLOWSTATE_PASSED_CHALLENGE SelfServiceLoginFlowState = "passed_challenge" +) + +func (v *SelfServiceLoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := SelfServiceLoginFlowState(value) + for _, existing := range []SelfServiceLoginFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid SelfServiceLoginFlowState", value) +} + +// Ptr returns reference to SelfServiceLoginFlowState value +func (v SelfServiceLoginFlowState) Ptr() *SelfServiceLoginFlowState { + return &v +} + +type NullableSelfServiceLoginFlowState struct { + value *SelfServiceLoginFlowState + isSet bool +} + +func (v NullableSelfServiceLoginFlowState) Get() *SelfServiceLoginFlowState { + return v.value +} + +func (v *NullableSelfServiceLoginFlowState) Set(val *SelfServiceLoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableSelfServiceLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableSelfServiceLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSelfServiceLoginFlowState(val *SelfServiceLoginFlowState) *NullableSelfServiceLoginFlowState { + return &NullableSelfServiceLoginFlowState{value: val, isSet: true} +} + +func (v NullableSelfServiceLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSelfServiceLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_self_service_recovery_flow_state.go b/internal/client-go/model_self_service_recovery_flow_state.go index efb98b8d127e..ae492a51d26b 100644 --- a/internal/client-go/model_self_service_recovery_flow_state.go +++ b/internal/client-go/model_self_service_recovery_flow_state.go @@ -1,13 +1,10 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - /* - * Ory Kratos API + * Ory Identities API * - * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. * * API version: - * Contact: hi@ory.sh + * Contact: office@ory.sh */ // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. @@ -22,7 +19,7 @@ import ( // SelfServiceRecoveryFlowState The state represents the state of the recovery flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type SelfServiceRecoveryFlowState string -// List of selfServiceRecoveryFlowState +// List of SelfServiceRecoveryFlowState const ( SELFSERVICERECOVERYFLOWSTATE_CHOOSE_METHOD SelfServiceRecoveryFlowState = "choose_method" SELFSERVICERECOVERYFLOWSTATE_SENT_EMAIL SelfServiceRecoveryFlowState = "sent_email" @@ -46,7 +43,7 @@ func (v *SelfServiceRecoveryFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SelfServiceRecoveryFlowState", value) } -// Ptr returns reference to selfServiceRecoveryFlowState value +// Ptr returns reference to SelfServiceRecoveryFlowState value func (v SelfServiceRecoveryFlowState) Ptr() *SelfServiceRecoveryFlowState { return &v } diff --git a/internal/client-go/model_self_service_registration_flow_state.go b/internal/client-go/model_self_service_registration_flow_state.go new file mode 100644 index 000000000000..a84387784ef1 --- /dev/null +++ b/internal/client-go/model_self_service_registration_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// SelfServiceRegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +type SelfServiceRegistrationFlowState string + +// List of SelfServiceRegistrationFlowState +const ( + SELFSERVICEREGISTRATIONFLOWSTATE_CHOOSE_METHOD SelfServiceRegistrationFlowState = "choose_method" + SELFSERVICEREGISTRATIONFLOWSTATE_SENT_EMAIL SelfServiceRegistrationFlowState = "sent_email" + SELFSERVICEREGISTRATIONFLOWSTATE_PASSED_CHALLENGE SelfServiceRegistrationFlowState = "passed_challenge" +) + +func (v *SelfServiceRegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := SelfServiceRegistrationFlowState(value) + for _, existing := range []SelfServiceRegistrationFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid SelfServiceRegistrationFlowState", value) +} + +// Ptr returns reference to SelfServiceRegistrationFlowState value +func (v SelfServiceRegistrationFlowState) Ptr() *SelfServiceRegistrationFlowState { + return &v +} + +type NullableSelfServiceRegistrationFlowState struct { + value *SelfServiceRegistrationFlowState + isSet bool +} + +func (v NullableSelfServiceRegistrationFlowState) Get() *SelfServiceRegistrationFlowState { + return v.value +} + +func (v *NullableSelfServiceRegistrationFlowState) Set(val *SelfServiceRegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableSelfServiceRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableSelfServiceRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSelfServiceRegistrationFlowState(val *SelfServiceRegistrationFlowState) *NullableSelfServiceRegistrationFlowState { + return &NullableSelfServiceRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableSelfServiceRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSelfServiceRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_self_service_settings_flow_state.go b/internal/client-go/model_self_service_settings_flow_state.go index 3a95e53bf169..9163efb643e5 100644 --- a/internal/client-go/model_self_service_settings_flow_state.go +++ b/internal/client-go/model_self_service_settings_flow_state.go @@ -1,13 +1,10 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - /* - * Ory Kratos API + * Ory Identities API * - * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. * * API version: - * Contact: hi@ory.sh + * Contact: office@ory.sh */ // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. @@ -22,7 +19,7 @@ import ( // SelfServiceSettingsFlowState show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. type SelfServiceSettingsFlowState string -// List of selfServiceSettingsFlowState +// List of SelfServiceSettingsFlowState const ( SELFSERVICESETTINGSFLOWSTATE_SHOW_FORM SelfServiceSettingsFlowState = "show_form" SELFSERVICESETTINGSFLOWSTATE_SUCCESS SelfServiceSettingsFlowState = "success" @@ -45,7 +42,7 @@ func (v *SelfServiceSettingsFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SelfServiceSettingsFlowState", value) } -// Ptr returns reference to selfServiceSettingsFlowState value +// Ptr returns reference to SelfServiceSettingsFlowState value func (v SelfServiceSettingsFlowState) Ptr() *SelfServiceSettingsFlowState { return &v } diff --git a/internal/client-go/model_self_service_verification_flow_state.go b/internal/client-go/model_self_service_verification_flow_state.go index 03937f84bd45..a3b9691ab038 100644 --- a/internal/client-go/model_self_service_verification_flow_state.go +++ b/internal/client-go/model_self_service_verification_flow_state.go @@ -1,13 +1,10 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - /* - * Ory Kratos API + * Ory Identities API * - * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. * * API version: - * Contact: hi@ory.sh + * Contact: office@ory.sh */ // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. @@ -22,7 +19,7 @@ import ( // SelfServiceVerificationFlowState The state represents the state of the verification flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type SelfServiceVerificationFlowState string -// List of selfServiceVerificationFlowState +// List of SelfServiceVerificationFlowState const ( SELFSERVICEVERIFICATIONFLOWSTATE_CHOOSE_METHOD SelfServiceVerificationFlowState = "choose_method" SELFSERVICEVERIFICATIONFLOWSTATE_SENT_EMAIL SelfServiceVerificationFlowState = "sent_email" @@ -46,7 +43,7 @@ func (v *SelfServiceVerificationFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SelfServiceVerificationFlowState", value) } -// Ptr returns reference to selfServiceVerificationFlowState value +// Ptr returns reference to SelfServiceVerificationFlowState value func (v SelfServiceVerificationFlowState) Ptr() *SelfServiceVerificationFlowState { return &v } diff --git a/internal/client-go/model_settings_flow.go b/internal/client-go/model_settings_flow.go index a1dc0aa98dc6..fa5cd9317c54 100644 --- a/internal/client-go/model_settings_flow.go +++ b/internal/client-go/model_settings_flow.go @@ -32,8 +32,9 @@ type SettingsFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State SettingsFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -43,7 +44,7 @@ type SettingsFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SettingsFlowState, type_ string, ui UiContainer) *SettingsFlow { +func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *SettingsFlow { this := SettingsFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -281,9 +282,10 @@ func (o *SettingsFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *SettingsFlow) GetState() SettingsFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *SettingsFlow) GetState() interface{} { if o == nil { - var ret SettingsFlowState + var ret interface{} return ret } @@ -292,15 +294,16 @@ func (o *SettingsFlow) GetState() SettingsFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *SettingsFlow) GetStateOk() (*SettingsFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *SettingsFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *SettingsFlow) SetState(v SettingsFlowState) { +func (o *SettingsFlow) SetState(v interface{}) { o.State = v } @@ -378,7 +381,7 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/client-go/model_update_login_flow_body.go b/internal/client-go/model_update_login_flow_body.go index 1aa032062a4b..36033328e78d 100644 --- a/internal/client-go/model_update_login_flow_body.go +++ b/internal/client-go/model_update_login_flow_body.go @@ -18,6 +18,7 @@ import ( // UpdateLoginFlowBody - struct for UpdateLoginFlowBody type UpdateLoginFlowBody struct { + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod @@ -25,6 +26,13 @@ type UpdateLoginFlowBody struct { UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod } +// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCodeMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithCodeMethod: v, + } +} + // UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -64,6 +72,19 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateLoginFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) + if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateLoginFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateLoginFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) if err == nil { @@ -131,6 +152,7 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateLoginFlowWithCodeMethod = nil dst.UpdateLoginFlowWithLookupSecretMethod = nil dst.UpdateLoginFlowWithOidcMethod = nil dst.UpdateLoginFlowWithPasswordMethod = nil @@ -147,6 +169,10 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateLoginFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) + } + if src.UpdateLoginFlowWithLookupSecretMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) } @@ -175,6 +201,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateLoginFlowWithCodeMethod != nil { + return obj.UpdateLoginFlowWithCodeMethod + } + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { return obj.UpdateLoginFlowWithLookupSecretMethod } diff --git a/internal/client-go/model_update_login_flow_with_code_method.go b/internal/client-go/model_update_login_flow_with_code_method.go new file mode 100644 index 000000000000..bd97ab583ebc --- /dev/null +++ b/internal/client-go/model_update_login_flow_with_code_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithCodeMethod Update Login flow using the code method +type UpdateLoginFlowWithCodeMethod struct { + // Code is the 6 digits code sent to the user + Code *string `json:"code,omitempty"` + // CSRFToken is the anti-CSRF token + CsrfToken string `json:"csrf_token"` + // Identifier is the code identifier The identifier requires that the user has already completed the registration or settings with code flow. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + Method string `json:"method"` + // Resend is set when the user wants to resend the code + Resend *string `json:"resend,omitempty"` +} + +// NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithCodeMethod(csrfToken string, method string) *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + this.CsrfToken = csrfToken + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithCodeMethodWithDefaults instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifier() string { + if o == nil || o.Identifier == nil { + var ret string + return ret + } + return *o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifierOk() (*string, bool) { + if o == nil || o.Identifier == nil { + return nil, false + } + return o.Identifier, true +} + +// HasIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasIdentifier() bool { + if o != nil && o.Identifier != nil { + return true + } + + return false +} + +// SetIdentifier gets a reference to the given string and assigns it to the Identifier field. +func (o *UpdateLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if true { + toSerialize["csrf_token"] = o.CsrfToken + } + if o.Identifier != nil { + toSerialize["identifier"] = o.Identifier + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithCodeMethod struct { + value *UpdateLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithCodeMethod) Get() *UpdateLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Set(val *UpdateLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithCodeMethod(val *UpdateLoginFlowWithCodeMethod) *NullableUpdateLoginFlowWithCodeMethod { + return &NullableUpdateLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go index 1a662fc62416..0e36a95f635f 100644 --- a/internal/client-go/model_update_registration_flow_body.go +++ b/internal/client-go/model_update_registration_flow_body.go @@ -18,11 +18,19 @@ import ( // UpdateRegistrationFlowBody - Update Registration Request Body type UpdateRegistrationFlowBody struct { + UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } +// UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithCodeMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithCodeMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithCodeMethod: v, + } +} + // UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithOidcMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithOidcMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -48,6 +56,19 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) + if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateRegistrationFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateRegistrationFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) if err == nil { @@ -89,6 +110,7 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateRegistrationFlowWithCodeMethod = nil dst.UpdateRegistrationFlowWithOidcMethod = nil dst.UpdateRegistrationFlowWithPasswordMethod = nil dst.UpdateRegistrationFlowWithWebAuthnMethod = nil @@ -103,6 +125,10 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRegistrationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithCodeMethod) + } + if src.UpdateRegistrationFlowWithOidcMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } @@ -123,6 +149,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return obj.UpdateRegistrationFlowWithCodeMethod + } + if obj.UpdateRegistrationFlowWithOidcMethod != nil { return obj.UpdateRegistrationFlowWithOidcMethod } diff --git a/internal/client-go/model_update_registration_flow_with_code_method.go b/internal/client-go/model_update_registration_flow_with_code_method.go new file mode 100644 index 000000000000..46b9126d666f --- /dev/null +++ b/internal/client-go/model_update_registration_flow_with_code_method.go @@ -0,0 +1,286 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithCodeMethod Update Registration Flow with Code Method +type UpdateRegistrationFlowWithCodeMethod struct { + // The OTP Code sent to the user + Code *string `json:"code,omitempty"` + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `code` when using the code method. + Method string `json:"method"` + // Resend restarts the flow with a new code + Resend *string `json:"resend,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithCodeMethod instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithCodeMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithCodeMethodWithDefaults instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithCodeMethodWithDefaults() *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithCodeMethod struct { + value *UpdateRegistrationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) Get() *UpdateRegistrationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Set(val *UpdateRegistrationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithCodeMethod(val *UpdateRegistrationFlowWithCodeMethod) *NullableUpdateRegistrationFlowWithCodeMethod { + return &NullableUpdateRegistrationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_verification_flow.go b/internal/client-go/model_verification_flow.go index 5190da660254..c10870c9f841 100644 --- a/internal/client-go/model_verification_flow.go +++ b/internal/client-go/model_verification_flow.go @@ -29,8 +29,9 @@ type VerificationFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl *string `json:"request_url,omitempty"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State VerificationFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type VerificationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewVerificationFlow(id string, state VerificationFlowState, type_ string, ui UiContainer) *VerificationFlow { +func NewVerificationFlow(id string, state interface{}, type_ string, ui UiContainer) *VerificationFlow { this := VerificationFlow{} this.Id = id this.State = state @@ -242,9 +243,10 @@ func (o *VerificationFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *VerificationFlow) GetState() VerificationFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *VerificationFlow) GetState() interface{} { if o == nil { - var ret VerificationFlowState + var ret interface{} return ret } @@ -253,15 +255,16 @@ func (o *VerificationFlow) GetState() VerificationFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *VerificationFlow) GetStateOk() (*VerificationFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *VerificationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *VerificationFlow) SetState(v VerificationFlowState) { +func (o *VerificationFlow) SetState(v interface{}) { o.State = v } @@ -333,7 +336,7 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index af0c731e5f92..7018bd681eac 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -35,6 +35,7 @@ docs/HealthStatus.md docs/Identity.md docs/IdentityApi.md docs/IdentityCredentials.md +docs/IdentityCredentialsCode.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -53,6 +54,7 @@ docs/IsAlive200Response.md docs/IsReady503Response.md docs/JsonPatch.md docs/LoginFlow.md +docs/LoginFlowState.md docs/LogoutFlow.md docs/Message.md docs/MessageDispatch.md @@ -70,6 +72,7 @@ docs/RecoveryFlowState.md docs/RecoveryIdentityAddress.md docs/RecoveryLinkForIdentity.md docs/RegistrationFlow.md +docs/RegistrationFlowState.md docs/SelfServiceFlowExpiredError.md docs/Session.md docs/SessionAuthenticationMethod.md @@ -93,6 +96,7 @@ docs/UiNodeTextAttributes.md docs/UiText.md docs/UpdateIdentityBody.md docs/UpdateLoginFlowBody.md +docs/UpdateLoginFlowWithCodeMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md docs/UpdateLoginFlowWithPasswordMethod.md @@ -102,6 +106,7 @@ docs/UpdateRecoveryFlowBody.md docs/UpdateRecoveryFlowWithCodeMethod.md docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md +docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md @@ -145,6 +150,7 @@ model_health_not_ready_status.go model_health_status.go model_identity.go model_identity_credentials.go +model_identity_credentials_code.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go model_identity_credentials_password.go @@ -163,6 +169,7 @@ model_is_alive_200_response.go model_is_ready_503_response.go model_json_patch.go model_login_flow.go +model_login_flow_state.go model_logout_flow.go model_message.go model_message_dispatch.go @@ -179,6 +186,7 @@ model_recovery_flow_state.go model_recovery_identity_address.go model_recovery_link_for_identity.go model_registration_flow.go +model_registration_flow_state.go model_self_service_flow_expired_error.go model_session.go model_session_authentication_method.go @@ -202,6 +210,7 @@ model_ui_node_text_attributes.go model_ui_text.go model_update_identity_body.go model_update_login_flow_body.go +model_update_login_flow_with_code_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go model_update_login_flow_with_password_method.go @@ -211,6 +220,7 @@ model_update_recovery_flow_body.go model_update_recovery_flow_with_code_method.go model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go +model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go model_update_registration_flow_with_password_method.go model_update_registration_flow_with_web_authn_method.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index cb48b260e91f..084b578785ee 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -159,6 +159,7 @@ Class | Method | HTTP request | Description - [HealthStatus](docs/HealthStatus.md) - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) + - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) @@ -177,6 +178,7 @@ Class | Method | HTTP request | Description - [IsReady503Response](docs/IsReady503Response.md) - [JsonPatch](docs/JsonPatch.md) - [LoginFlow](docs/LoginFlow.md) + - [LoginFlowState](docs/LoginFlowState.md) - [LogoutFlow](docs/LogoutFlow.md) - [Message](docs/Message.md) - [MessageDispatch](docs/MessageDispatch.md) @@ -193,6 +195,7 @@ Class | Method | HTTP request | Description - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) - [RegistrationFlow](docs/RegistrationFlow.md) + - [RegistrationFlowState](docs/RegistrationFlowState.md) - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) - [Session](docs/Session.md) - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) @@ -216,6 +219,7 @@ Class | Method | HTTP request | Description - [UiText](docs/UiText.md) - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) + - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) @@ -225,6 +229,7 @@ Class | Method | HTTP request | Description - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) + - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) diff --git a/internal/httpclient/model_identity_credentials_code.go b/internal/httpclient/model_identity_credentials_code.go new file mode 100644 index 000000000000..f542b359639a --- /dev/null +++ b/internal/httpclient/model_identity_credentials_code.go @@ -0,0 +1,162 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "time" +) + +// IdentityCredentialsCode CredentialsCode represents a one time login/registration code +type IdentityCredentialsCode struct { + AddressType *string `json:"address_type,omitempty"` + UsedAt NullableTime `json:"used_at,omitempty"` +} + +// NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsCode() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// NewIdentityCredentialsCodeWithDefaults instantiates a new IdentityCredentialsCode object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// GetAddressType returns the AddressType field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddressType() string { + if o == nil || o.AddressType == nil { + var ret string + return ret + } + return *o.AddressType +} + +// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { + if o == nil || o.AddressType == nil { + return nil, false + } + return o.AddressType, true +} + +// HasAddressType returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddressType() bool { + if o != nil && o.AddressType != nil { + return true + } + + return false +} + +// SetAddressType gets a reference to the given string and assigns it to the AddressType field. +func (o *IdentityCredentialsCode) SetAddressType(v string) { + o.AddressType = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityCredentialsCode) GetUsedAt() time.Time { + if o == nil || o.UsedAt.Get() == nil { + var ret time.Time + return ret + } + return *o.UsedAt.Get() +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return o.UsedAt.Get(), o.UsedAt.IsSet() +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasUsedAt() bool { + if o != nil && o.UsedAt.IsSet() { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. +func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { + o.UsedAt.Set(&v) +} + +// SetUsedAtNil sets the value for UsedAt to be an explicit nil +func (o *IdentityCredentialsCode) SetUsedAtNil() { + o.UsedAt.Set(nil) +} + +// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil +func (o *IdentityCredentialsCode) UnsetUsedAt() { + o.UsedAt.Unset() +} + +func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.AddressType != nil { + toSerialize["address_type"] = o.AddressType + } + if o.UsedAt.IsSet() { + toSerialize["used_at"] = o.UsedAt.Get() + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsCode struct { + value *IdentityCredentialsCode + isSet bool +} + +func (v NullableIdentityCredentialsCode) Get() *IdentityCredentialsCode { + return v.value +} + +func (v *NullableIdentityCredentialsCode) Set(val *IdentityCredentialsCode) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCode) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCode(val *IdentityCredentialsCode) *NullableIdentityCredentialsCode { + return &NullableIdentityCredentialsCode{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_identity_credentials_type.go b/internal/httpclient/model_identity_credentials_type.go index f02f3f19b898..a9d01c54e796 100644 --- a/internal/httpclient/model_identity_credentials_type.go +++ b/internal/httpclient/model_identity_credentials_type.go @@ -26,6 +26,7 @@ const ( IDENTITYCREDENTIALSTYPE_OIDC IdentityCredentialsType = "oidc" IDENTITYCREDENTIALSTYPE_WEBAUTHN IdentityCredentialsType = "webauthn" IDENTITYCREDENTIALSTYPE_LOOKUP_SECRET IdentityCredentialsType = "lookup_secret" + IDENTITYCREDENTIALSTYPE_CODE IdentityCredentialsType = "code" ) func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { @@ -35,7 +36,7 @@ func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { return err } enumTypeValue := IdentityCredentialsType(value) - for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret"} { + for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret", "code"} { if existing == enumTypeValue { *v = enumTypeValue return nil diff --git a/internal/httpclient/model_login_flow.go b/internal/httpclient/model_login_flow.go index 1b3f4b6c7dde..dc7c67ea2649 100644 --- a/internal/httpclient/model_login_flow.go +++ b/internal/httpclient/model_login_flow.go @@ -39,6 +39,8 @@ type LoginFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the login flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -50,12 +52,13 @@ type LoginFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *LoginFlow { +func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *LoginFlow { this := LoginFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -421,6 +424,32 @@ func (o *LoginFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *LoginFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *LoginFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *LoginFlow) SetState(v interface{}) { + o.State = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -539,6 +568,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_login_flow_state.go b/internal/httpclient/model_login_flow_state.go new file mode 100644 index 000000000000..ce5570b79032 --- /dev/null +++ b/internal/httpclient/model_login_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// LoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +type LoginFlowState string + +// List of loginFlowState +const ( + LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" + LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" + LOGINFLOWSTATE_PASSED_CHALLENGE LoginFlowState = "passed_challenge" +) + +func (v *LoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := LoginFlowState(value) + for _, existing := range []LoginFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid LoginFlowState", value) +} + +// Ptr returns reference to loginFlowState value +func (v LoginFlowState) Ptr() *LoginFlowState { + return &v +} + +type NullableLoginFlowState struct { + value *LoginFlowState + isSet bool +} + +func (v NullableLoginFlowState) Get() *LoginFlowState { + return v.value +} + +func (v *NullableLoginFlowState) Set(val *LoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowState(val *LoginFlowState) *NullableLoginFlowState { + return &NullableLoginFlowState{value: val, isSet: true} +} + +func (v NullableLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_message.go b/internal/httpclient/model_message.go index 3a6f3f92362b..f0452185f169 100644 --- a/internal/httpclient/model_message.go +++ b/internal/httpclient/model_message.go @@ -28,7 +28,7 @@ type Message struct { SendCount int64 `json:"send_count"` Status CourierMessageStatus `json:"status"` Subject string `json:"subject"` - // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid TemplateType string `json:"template_type"` Type CourierMessageType `json:"type"` // UpdatedAt is a helper struct field for gobuffalo.pop. diff --git a/internal/httpclient/model_recovery_flow.go b/internal/httpclient/model_recovery_flow.go index 6ae19ebd60e6..acf4ff667df3 100644 --- a/internal/httpclient/model_recovery_flow.go +++ b/internal/httpclient/model_recovery_flow.go @@ -29,8 +29,9 @@ type RecoveryFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State RecoveryFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type RecoveryFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state RecoveryFlowState, type_ string, ui UiContainer) *RecoveryFlow { +func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RecoveryFlow { this := RecoveryFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -221,9 +222,10 @@ func (o *RecoveryFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *RecoveryFlow) GetState() RecoveryFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RecoveryFlow) GetState() interface{} { if o == nil { - var ret RecoveryFlowState + var ret interface{} return ret } @@ -232,15 +234,16 @@ func (o *RecoveryFlow) GetState() RecoveryFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *RecoveryFlow) GetStateOk() (*RecoveryFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RecoveryFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *RecoveryFlow) SetState(v RecoveryFlowState) { +func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } @@ -312,7 +315,7 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/httpclient/model_registration_flow.go b/internal/httpclient/model_registration_flow.go index fe9f697b5551..9b08288d6a16 100644 --- a/internal/httpclient/model_registration_flow.go +++ b/internal/httpclient/model_registration_flow.go @@ -34,6 +34,8 @@ type RegistrationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. + State interface{} `json:"state"` // TransientPayload is used to pass data from the registration to a webhook TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. @@ -45,12 +47,13 @@ type RegistrationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *RegistrationFlow { +func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RegistrationFlow { this := RegistrationFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -320,6 +323,32 @@ func (o *RegistrationFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RegistrationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RegistrationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *RegistrationFlow) SetState(v interface{}) { + o.State = v +} + // GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. func (o *RegistrationFlow) GetTransientPayload() map[string]interface{} { if o == nil || o.TransientPayload == nil { @@ -429,6 +458,9 @@ func (o RegistrationFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if o.TransientPayload != nil { toSerialize["transient_payload"] = o.TransientPayload } diff --git a/internal/httpclient/model_registration_flow_state.go b/internal/httpclient/model_registration_flow_state.go new file mode 100644 index 000000000000..86f3fd38cff0 --- /dev/null +++ b/internal/httpclient/model_registration_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// RegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +type RegistrationFlowState string + +// List of registrationFlowState +const ( + REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" + REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" + REGISTRATIONFLOWSTATE_PASSED_CHALLENGE RegistrationFlowState = "passed_challenge" +) + +func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RegistrationFlowState(value) + for _, existing := range []RegistrationFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) +} + +// Ptr returns reference to registrationFlowState value +func (v RegistrationFlowState) Ptr() *RegistrationFlowState { + return &v +} + +type NullableRegistrationFlowState struct { + value *RegistrationFlowState + isSet bool +} + +func (v NullableRegistrationFlowState) Get() *RegistrationFlowState { + return v.value +} + +func (v *NullableRegistrationFlowState) Set(val *RegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlowState(val *RegistrationFlowState) *NullableRegistrationFlowState { + return &NullableRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_settings_flow.go b/internal/httpclient/model_settings_flow.go index a1dc0aa98dc6..fa5cd9317c54 100644 --- a/internal/httpclient/model_settings_flow.go +++ b/internal/httpclient/model_settings_flow.go @@ -32,8 +32,9 @@ type SettingsFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State SettingsFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -43,7 +44,7 @@ type SettingsFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SettingsFlowState, type_ string, ui UiContainer) *SettingsFlow { +func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *SettingsFlow { this := SettingsFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -281,9 +282,10 @@ func (o *SettingsFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *SettingsFlow) GetState() SettingsFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *SettingsFlow) GetState() interface{} { if o == nil { - var ret SettingsFlowState + var ret interface{} return ret } @@ -292,15 +294,16 @@ func (o *SettingsFlow) GetState() SettingsFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *SettingsFlow) GetStateOk() (*SettingsFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *SettingsFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *SettingsFlow) SetState(v SettingsFlowState) { +func (o *SettingsFlow) SetState(v interface{}) { o.State = v } @@ -378,7 +381,7 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/httpclient/model_update_login_flow_body.go b/internal/httpclient/model_update_login_flow_body.go index 1aa032062a4b..36033328e78d 100644 --- a/internal/httpclient/model_update_login_flow_body.go +++ b/internal/httpclient/model_update_login_flow_body.go @@ -18,6 +18,7 @@ import ( // UpdateLoginFlowBody - struct for UpdateLoginFlowBody type UpdateLoginFlowBody struct { + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod @@ -25,6 +26,13 @@ type UpdateLoginFlowBody struct { UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod } +// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCodeMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithCodeMethod: v, + } +} + // UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -64,6 +72,19 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateLoginFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) + if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateLoginFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateLoginFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) if err == nil { @@ -131,6 +152,7 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateLoginFlowWithCodeMethod = nil dst.UpdateLoginFlowWithLookupSecretMethod = nil dst.UpdateLoginFlowWithOidcMethod = nil dst.UpdateLoginFlowWithPasswordMethod = nil @@ -147,6 +169,10 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateLoginFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) + } + if src.UpdateLoginFlowWithLookupSecretMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) } @@ -175,6 +201,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateLoginFlowWithCodeMethod != nil { + return obj.UpdateLoginFlowWithCodeMethod + } + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { return obj.UpdateLoginFlowWithLookupSecretMethod } diff --git a/internal/httpclient/model_update_login_flow_with_code_method.go b/internal/httpclient/model_update_login_flow_with_code_method.go new file mode 100644 index 000000000000..bd97ab583ebc --- /dev/null +++ b/internal/httpclient/model_update_login_flow_with_code_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithCodeMethod Update Login flow using the code method +type UpdateLoginFlowWithCodeMethod struct { + // Code is the 6 digits code sent to the user + Code *string `json:"code,omitempty"` + // CSRFToken is the anti-CSRF token + CsrfToken string `json:"csrf_token"` + // Identifier is the code identifier The identifier requires that the user has already completed the registration or settings with code flow. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + Method string `json:"method"` + // Resend is set when the user wants to resend the code + Resend *string `json:"resend,omitempty"` +} + +// NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithCodeMethod(csrfToken string, method string) *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + this.CsrfToken = csrfToken + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithCodeMethodWithDefaults instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifier() string { + if o == nil || o.Identifier == nil { + var ret string + return ret + } + return *o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifierOk() (*string, bool) { + if o == nil || o.Identifier == nil { + return nil, false + } + return o.Identifier, true +} + +// HasIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasIdentifier() bool { + if o != nil && o.Identifier != nil { + return true + } + + return false +} + +// SetIdentifier gets a reference to the given string and assigns it to the Identifier field. +func (o *UpdateLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if true { + toSerialize["csrf_token"] = o.CsrfToken + } + if o.Identifier != nil { + toSerialize["identifier"] = o.Identifier + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithCodeMethod struct { + value *UpdateLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithCodeMethod) Get() *UpdateLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Set(val *UpdateLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithCodeMethod(val *UpdateLoginFlowWithCodeMethod) *NullableUpdateLoginFlowWithCodeMethod { + return &NullableUpdateLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go index 1a662fc62416..0e36a95f635f 100644 --- a/internal/httpclient/model_update_registration_flow_body.go +++ b/internal/httpclient/model_update_registration_flow_body.go @@ -18,11 +18,19 @@ import ( // UpdateRegistrationFlowBody - Update Registration Request Body type UpdateRegistrationFlowBody struct { + UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } +// UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithCodeMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithCodeMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithCodeMethod: v, + } +} + // UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithOidcMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithOidcMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -48,6 +56,19 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) + if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateRegistrationFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateRegistrationFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) if err == nil { @@ -89,6 +110,7 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateRegistrationFlowWithCodeMethod = nil dst.UpdateRegistrationFlowWithOidcMethod = nil dst.UpdateRegistrationFlowWithPasswordMethod = nil dst.UpdateRegistrationFlowWithWebAuthnMethod = nil @@ -103,6 +125,10 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRegistrationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithCodeMethod) + } + if src.UpdateRegistrationFlowWithOidcMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } @@ -123,6 +149,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return obj.UpdateRegistrationFlowWithCodeMethod + } + if obj.UpdateRegistrationFlowWithOidcMethod != nil { return obj.UpdateRegistrationFlowWithOidcMethod } diff --git a/internal/httpclient/model_update_registration_flow_with_code_method.go b/internal/httpclient/model_update_registration_flow_with_code_method.go new file mode 100644 index 000000000000..46b9126d666f --- /dev/null +++ b/internal/httpclient/model_update_registration_flow_with_code_method.go @@ -0,0 +1,286 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithCodeMethod Update Registration Flow with Code Method +type UpdateRegistrationFlowWithCodeMethod struct { + // The OTP Code sent to the user + Code *string `json:"code,omitempty"` + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `code` when using the code method. + Method string `json:"method"` + // Resend restarts the flow with a new code + Resend *string `json:"resend,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithCodeMethod instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithCodeMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithCodeMethodWithDefaults instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithCodeMethodWithDefaults() *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithCodeMethod struct { + value *UpdateRegistrationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) Get() *UpdateRegistrationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Set(val *UpdateRegistrationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithCodeMethod(val *UpdateRegistrationFlowWithCodeMethod) *NullableUpdateRegistrationFlowWithCodeMethod { + return &NullableUpdateRegistrationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_verification_flow.go b/internal/httpclient/model_verification_flow.go index 5190da660254..c10870c9f841 100644 --- a/internal/httpclient/model_verification_flow.go +++ b/internal/httpclient/model_verification_flow.go @@ -29,8 +29,9 @@ type VerificationFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl *string `json:"request_url,omitempty"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State VerificationFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type VerificationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewVerificationFlow(id string, state VerificationFlowState, type_ string, ui UiContainer) *VerificationFlow { +func NewVerificationFlow(id string, state interface{}, type_ string, ui UiContainer) *VerificationFlow { this := VerificationFlow{} this.Id = id this.State = state @@ -242,9 +243,10 @@ func (o *VerificationFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *VerificationFlow) GetState() VerificationFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *VerificationFlow) GetState() interface{} { if o == nil { - var ret VerificationFlowState + var ret interface{} return ret } @@ -253,15 +255,16 @@ func (o *VerificationFlow) GetState() VerificationFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *VerificationFlow) GetStateOk() (*VerificationFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *VerificationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *VerificationFlow) SetState(v VerificationFlowState) { +func (o *VerificationFlow) SetState(v interface{}) { o.State = v } @@ -333,7 +336,7 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/testhelpers/courier.go b/internal/testhelpers/courier.go index 825ff4c0ec6d..fd9aa63f45d2 100644 --- a/internal/testhelpers/courier.go +++ b/internal/testhelpers/courier.go @@ -6,25 +6,38 @@ package testhelpers import ( "context" "regexp" + "sort" "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/kratos/courier" + "github.com/ory/x/pagination/keysetpagination" ) -func CourierExpectMessage(t *testing.T, reg interface { +func CourierExpectMessage(ctx context.Context, t *testing.T, reg interface { courier.PersistenceProvider -}, recipient, subject string) *courier.Message { - message, err := reg.CourierPersister().LatestQueuedMessage(context.Background()) +}, recipient, subject string, +) *courier.Message { + messages, total, _, err := reg.CourierPersister().ListMessages(ctx, courier.ListCourierMessagesParameters{ + Recipient: recipient, + }, []keysetpagination.Option{}) require.NoError(t, err) + require.GreaterOrEqual(t, total, int64(1)) - assert.EqualValues(t, subject, strings.TrimSpace(message.Subject)) - assert.EqualValues(t, recipient, strings.TrimSpace(message.Recipient)) + sort.Slice(messages, func(i, j int) bool { + return messages[i].CreatedAt.After(messages[j].CreatedAt) + }) - return message + for _, m := range messages { + if strings.EqualFold(m.Recipient, recipient) && strings.EqualFold(m.Subject, subject) { + return &m + } + } + + require.Failf(t, "could not find courier messages with recipient %s and subject %s", recipient, subject) + return nil } func CourierExpectLinkInMessage(t *testing.T, message *courier.Message, offset int) string { diff --git a/persistence/reference.go b/persistence/reference.go index 215ceb4a7f3f..56a7ca1712df 100644 --- a/persistence/reference.go +++ b/persistence/reference.go @@ -51,6 +51,8 @@ type Persister interface { link.VerificationTokenPersister code.RecoveryCodePersister code.VerificationCodePersister + code.RegistrationCodePersister + code.LoginCodePersister CleanupDatabase(context.Context, time.Duration, time.Duration, int) error Close(context.Context) error diff --git a/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json b/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json new file mode 100644 index 000000000000..bed9cbb51ee4 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json @@ -0,0 +1,17 @@ +{ + "id": "28ff0031-190b-4253-bd15-14308dec013e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbarbarfoo@ory.sh" + }, + "metadata_public": { + "foo": "bar" + }, + "metadata_admin": { + "baz": "bar" + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json b/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json new file mode 100644 index 000000000000..e695ce9e3ecf --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json @@ -0,0 +1,6 @@ +{ + "id": "bd292366-af32-4ba6-bdf0-11d6d1a217f3", + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z", + "identity_id": "28ff0031-190b-4253-bd15-14308dec013e" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json b/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json new file mode 100644 index 000000000000..17f770efbe90 --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json @@ -0,0 +1,18 @@ +{ + "id": "00b1517f-2467-4aaf-b0a5-82b4a27dcaf5", + "oauth2_login_challenge": "challenge data", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json b/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json index e48e54d97a6b..ce8841aa07ff 100644 --- a/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json +++ b/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json b/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json index 5f63a7ec006a..770f0b2e2c38 100644 --- a/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json +++ b/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal2" + "requested_aal": "aal2", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json b/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json index efbd0740cdfb..b6cd377d812f 100644 --- a/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json +++ b/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json b/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json index 7586d19409ab..effdc9f1f2a0 100644 --- a/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json +++ b/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal2" + "requested_aal": "aal2", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json b/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json index 084b36a0c0b9..6eac76a4e91b 100644 --- a/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json +++ b/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal2" + "requested_aal": "aal2", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json b/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json index 13dff119fce0..577b054917db 100644 --- a/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json +++ b/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json b/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json index 5f1529c393b3..6f0fae29f575 100644 --- a/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json +++ b/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json @@ -13,5 +13,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": true, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json b/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json index fe46265a6d2e..64a415dfba4a 100644 --- a/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json +++ b/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json b/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json index 85156c189e4d..e2ccb8f7616d 100644 --- a/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json +++ b/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json b/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json index c38727386af7..863594687d00 100644 --- a/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json +++ b/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json b/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json index eb8ec21e0e31..138f4838c466 100644 --- a/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json +++ b/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json b/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json index 418e16ebe69b..41bc0e84748f 100644 --- a/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json +++ b/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json b/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json index 84eda2f96615..ae28f38c8fe4 100644 --- a/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json +++ b/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json index b3f93459b975..a2b9861acec7 100644 --- a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json +++ b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json @@ -13,5 +13,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json index 438fb4005e14..e2d58f6dc1fe 100644 --- a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json +++ b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json @@ -13,5 +13,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json b/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json index 87ccb1d1dcd0..00a1a2d7c3e5 100644 --- a/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json +++ b/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json new file mode 100644 index 000000000000..5e429ce3d9ca --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json @@ -0,0 +1,5 @@ +{ + "id": "f1f66a69-ce02-4a12-9591-9e02dda30a0d", + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json b/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json index 1e649d64ad51..ccfcf94814a5 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json +++ b/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json @@ -8,5 +8,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json b/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json index 7f90a694387d..5c110a3394f5 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json +++ b/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json @@ -8,5 +8,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json b/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json index dbc832d2aa71..8df52efff06b 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json +++ b/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json @@ -8,5 +8,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json b/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json index 6b627d7541f9..d58beb9edffc 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json +++ b/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json new file mode 100644 index 000000000000..6179619c71fe --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json @@ -0,0 +1,14 @@ +{ + "id": "69c80296-36cd-4afc-921a-15369cac5bf0", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "choose_method" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json b/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json index 6a1dcdac29dd..19104b6d9f26 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json +++ b/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json b/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json index ed2e8512fde1..616af278cd82 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json +++ b/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json b/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json index df3f9c392998..a1f323ba3c4d 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json +++ b/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json b/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json index 2195263f1574..1e6cc2579af2 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json +++ b/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json b/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json index 497f88de81b2..560741f9a18d 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json +++ b/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json index 8947653d90e7..ce1272433edf 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json +++ b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json index 6763bf5c63f5..4d1d58bdaf51 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json +++ b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json b/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json index d894073c5468..c7d1b8207a4e 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json +++ b/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 36126f147935..798afd54dc79 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -73,7 +73,8 @@ func CompareWithFixture(t *testing.T, actual interface{}, prefix string, id stri func TestMigrations_SQLite(t *testing.T) { t.Parallel() sqlite, err := pop.NewConnection(&pop.ConnectionDetails{ - URL: "sqlite3://" + filepath.Join(os.TempDir(), x.NewUUID().String()) + ".sql?_fk=true"}) + URL: "sqlite3://" + filepath.Join(os.TempDir(), x.NewUUID().String()) + ".sql?_fk=true", + }) require.NoError(t, err) require.NoError(t, sqlite.Open()) @@ -105,7 +106,6 @@ func TestMigrations_Cockroach(t *testing.T) { } func testDatabase(t *testing.T, db string, c *pop.Connection) { - ctx := context.Background() l := logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel)) @@ -372,6 +372,40 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_code"), found) }) + t.Run("case=registration_code", func(t *testing.T) { + wg.Add(1) + defer wg.Done() + t.Parallel() + + var ids []code.RegistrationCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "registration_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "registration_code"), found) + }) + + t.Run("case=login_code", func(t *testing.T) { + wg.Add(1) + defer wg.Done() + t.Parallel() + + var ids []code.LoginCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "login_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "login_code"), found) + }) + t.Run("suite=constraints", func(t *testing.T) { // This is not really a parallel test, but we have to mark it parallel so the other tests run first. t.Parallel() diff --git a/persistence/sql/migratest/testdata/20230707133700_testdata.sql b/persistence/sql/migratest/testdata/20230707133700_testdata.sql new file mode 100644 index 000000000000..bcfc9bc12f58 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133700_testdata.sql @@ -0,0 +1,30 @@ +INSERT INTO selfservice_login_flows (id,nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, internal_context, oauth2_login_challenge_data, state) +VALUES ('00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', '{"foo":"bar"}', 'challenge data', 'choose_method'); + +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at, metadata_public, metadata_admin, + available_aal) +VALUES ('28ff0031-190b-4253-bd15-14308dec013e', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', + '{"email":"bazbarbarfoo@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '{"foo":"bar"}', '{"baz":"bar"}', + NULL); + +INSERT INTO identity_login_codes (id, code, address, address_type, used_at, expires_at, issued_at, selfservice_login_flow_id, identity_id, + created_at, updated_at, nid) +VALUES ('bd292366-af32-4ba6-bdf0-11d6d1a217f3', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +'bazbarbarfoo@ory.com', +'email', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', +'28ff0031-190b-4253-bd15-14308dec013e', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'884f556e-eb3a-4b9f-bee3-11345642c6c0' +) diff --git a/persistence/sql/migratest/testdata/20230707133701_testdata.sql b/persistence/sql/migratest/testdata/20230707133701_testdata.sql new file mode 100644 index 000000000000..8a256314ae95 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133701_testdata.sql @@ -0,0 +1,23 @@ +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, + created_at, updated_at, type, ui, internal_context, oauth2_login_challenge, state) +VALUES ('69c80296-36cd-4afc-921a-15369cac5bf0', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'browser', '{}', '{"foo":"bar"}', + '3caddfd5-9903-4bce-83ff-cae36f42dff7', 'choose_method'); + +INSERT INTO identity_registration_codes (id, address, address_type, code, used_at, expires_at, issued_at, selfservice_registration_flow_id, + created_at, updated_at, nid) +VALUES ('f1f66a69-ce02-4a12-9591-9e02dda30a0d', +'example@example.com', +'email', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'69c80296-36cd-4afc-921a-15369cac5bf0', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'884f556e-eb3a-4b9f-bee3-11345642c6c0' +) diff --git a/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql new file mode 100644 index 000000000000..ddd3c7bbfbc0 --- /dev/null +++ b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql @@ -0,0 +1,2 @@ +ALTER table selfservice_registration_flows DROP COLUMN state; +ALTER table selfservice_login_flows DROP COLUMN state; diff --git a/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql new file mode 100644 index 000000000000..26f4d0649508 --- /dev/null +++ b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql @@ -0,0 +1,2 @@ +ALTER table selfservice_login_flows ADD state VARCHAR(255) NULL; +ALTER table selfservice_registration_flows ADD state VARCHAR(255) NULL; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql new file mode 100644 index 000000000000..3738073d0aa9 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql @@ -0,0 +1,3 @@ +DROP TABLE identity_login_codes; + +ALTER TABLE selfservice_login_flows DROP submit_count; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql new file mode 100644 index 000000000000..9ddc6c72d8ac --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql @@ -0,0 +1,29 @@ +CREATE TABLE identity_login_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id CHAR(36), + identity_id CHAR(36) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql new file mode 100644 index 000000000000..7df0e9b00e21 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE identity_login_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id UUID NOT NULL, + identity_id UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid UUID NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql new file mode 100644 index 000000000000..d4211e92a776 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql @@ -0,0 +1,3 @@ +DROP TABLE identity_registration_codes; + +ALTER TABLE selfservice_registration_flows DROP submit_count; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql new file mode 100644 index 000000000000..36f049d58c1a --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql @@ -0,0 +1,27 @@ +CREATE TABLE identity_registration_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_registration_flow_id CHAR(36), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_registration_codes_selfservice_registration_flows_id_fk + FOREIGN KEY (selfservice_registration_flow_id) + REFERENCES selfservice_registration_flows (id) + ON DELETE cascade, + CONSTRAINT identity_registration_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid, selfservice_registration_flow_id); +CREATE INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes (id, nid); + +ALTER TABLE selfservice_registration_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql new file mode 100644 index 000000000000..0dc3a9879341 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql @@ -0,0 +1,27 @@ +CREATE TABLE identity_registration_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_registration_flow_id UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid UUID NOT NULL, + CONSTRAINT identity_registration_codes_selfservice_registration_flows_id_fk + FOREIGN KEY (selfservice_registration_flow_id) + REFERENCES selfservice_registration_flows (id) + ON DELETE cascade, + CONSTRAINT identity_registration_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid, selfservice_registration_flow_id); +CREATE INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes (id, nid); + +ALTER TABLE selfservice_registration_flows ADD submit_count int NOT NULL DEFAULT 0; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql new file mode 100644 index 000000000000..84f10f939a12 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'code'; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql new file mode 100644 index 000000000000..47e0cf0b2b34 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '14f3b7e2-8725-4068-be39-8a796485fd97', 'code' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'code'); diff --git a/persistence/sql/persister_code.go b/persistence/sql/persister_code.go new file mode 100644 index 000000000000..3b8103a36361 --- /dev/null +++ b/persistence/sql/persister_code.go @@ -0,0 +1,123 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "crypto/subtle" + "fmt" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +type oneTimeCodeProvider interface { + GetID() uuid.UUID + Validate() error + TableName(ctx context.Context) string + GetHMACCode() string +} + +type codeOptions struct { + IdentityID *uuid.UUID +} + +type codeOption func(o *codeOptions) + +func withCheckIdentityID(id uuid.UUID) codeOption { + return func(o *codeOptions) { + o.IdentityID = &id + } +} + +func useOneTimeCode[P any, U interface { + *P + oneTimeCodeProvider +}](ctx context.Context, p *Persister, flowID uuid.UUID, userProvidedCode string, flowTableName string, foreignKeyName string, opts ...codeOption) (U, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.useOneTimeCode") + defer span.End() + + o := new(codeOptions) + for _, opt := range opts { + opt(o) + } + + var target U + nid := p.NetworkID(ctx) + if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + //#nosec G201 -- TableName is static + if err := tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), flowID, nid).Exec(); err != nil { + return err + } + + var submitCount int + // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. + //#nosec G201 -- TableName is static + if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), flowID, nid).First(&submitCount)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction + return nil + } + return err + } + + // This check prevents parallel brute force attacks by checking the submit count inside this database + // transaction. If the flow has been submitted more than 5 times, the transaction is aborted (regardless of + // whether the code was correct or not) and we thus give no indication whether the supplied code was correct or + // not. For more explanation see [this comment](https://github.com/ory/kratos/pull/2645#discussion_r984732899). + if submitCount > 5 { + return errors.WithStack(code.ErrCodeSubmittedTooOften) + } + + var codes []U + codesQuery := tx.Where(fmt.Sprintf("nid = ? AND %s = ?", foreignKeyName), nid, flowID) + if o.IdentityID != nil { + codesQuery = codesQuery.Where("identity_id = ?", *o.IdentityID) + } + + if err := sqlcon.HandleError(codesQuery.All(&codes)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction and reset the submit count. + return nil + } + + return err + } + + secrets: + for _, secret := range p.r.Config().SecretsSession(ctx) { + suppliedCode := []byte(p.hmacValueWithSecret(ctx, userProvidedCode, secret)) + for i := range codes { + c := codes[i] + if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 { + // Not the supplied code + continue + } + target = c + break secrets + } + } + + if target.Validate() != nil { + // Return no error, as that would roll back the transaction + return nil + } + + //#nosec G201 -- TableName is static + return tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", target.TableName(ctx)), time.Now().UTC(), target.GetID(), nid).Exec() + }); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := target.Validate(); err != nil { + return nil, err + } + + return target, nil +} diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go index 1f29a1860e35..ec1da55babbb 100644 --- a/persistence/sql/persister_login.go +++ b/persistence/sql/persister_login.go @@ -9,7 +9,6 @@ import ( "time" "github.com/gobuffalo/pop/v6" - "github.com/gofrs/uuid" "github.com/ory/x/sqlcon" diff --git a/persistence/sql/persister_login_code.go b/persistence/sql/persister_login_code.go new file mode 100644 index 000000000000..3d5dd027826d --- /dev/null +++ b/persistence/sql/persister_login_code.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateLoginCode(ctx context.Context, params *code.CreateLoginCodeParams) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginCode") + defer span.End() + + now := time.Now().UTC() + loginCode := &code.LoginCode{ + IdentityID: params.IdentityID, + Address: params.Address, + AddressType: params.AddressType, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return loginCode, nil +} + +func (p *Persister) UseLoginCode(ctx context.Context, flowID uuid.UUID, identityID uuid.UUID, userProvidedCode string) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseLoginCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.LoginCode, *code.LoginCode](ctx, p, flowID, userProvidedCode, new(login.Flow).TableName(ctx), "selfservice_login_flow_id", withCheckIdentityID(identityID)) + if err != nil { + return nil, err + } + + return codeRow, nil +} + +func (p *Persister) GetUsedLoginCode(ctx context.Context, flowID uuid.UUID) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedLoginCode") + defer span.End() + + var loginCode code.LoginCode + if err := p.Connection(ctx).Where("selfservice_login_flow_id = ? AND nid = ? AND used_at IS NOT NULL", flowID, p.NetworkID(ctx)).First(&loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return &loginCode, nil +} + +func (p *Persister) DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteLoginCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_login_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.LoginCode{}) +} diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go index d34a6fabd435..8ac81cd009a5 100644 --- a/persistence/sql/persister_recovery.go +++ b/persistence/sql/persister_recovery.go @@ -5,7 +5,6 @@ package sql import ( "context" - "crypto/subtle" "fmt" "time" @@ -16,15 +15,15 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/persistence/sql/update" - "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" - "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/x/sqlcon" ) -var _ recovery.FlowPersister = new(Persister) -var _ link.RecoveryTokenPersister = new(Persister) +var ( + _ recovery.FlowPersister = new(Persister) + _ link.RecoveryTokenPersister = new(Persister) +) func (p *Persister) CreateRecoveryFlow(ctx context.Context, r *recovery.Flow) error { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryFlow") @@ -135,145 +134,3 @@ func (p *Persister) DeleteExpiredRecoveryFlows(ctx context.Context, expiresAt ti } return nil } - -func (p *Persister) CreateRecoveryCode(ctx context.Context, dto *code.CreateRecoveryCodeParams) (*code.RecoveryCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryCode") - defer span.End() - - now := time.Now() - - recoveryCode := &code.RecoveryCode{ - ID: uuid.Nil, - CodeHMAC: p.hmacValue(ctx, dto.RawCode), - ExpiresAt: now.UTC().Add(dto.ExpiresIn), - IssuedAt: now, - CodeType: dto.CodeType, - FlowID: dto.FlowID, - NID: p.NetworkID(ctx), - IdentityID: dto.IdentityID, - } - - if dto.RecoveryAddress != nil { - recoveryCode.RecoveryAddress = dto.RecoveryAddress - recoveryCode.RecoveryAddressID = uuid.NullUUID{ - UUID: dto.RecoveryAddress.ID, - Valid: true, - } - } - - // This should not create the request eagerly because otherwise we might accidentally create an address that isn't - // supposed to be in the database. - if err := p.GetConnection(ctx).Create(recoveryCode); err != nil { - return nil, err - } - - return recoveryCode, nil -} - -// UseRecoveryCode attempts to "use" the supplied code in the flow -// -// If the supplied code matched a code from the flow, no error is returned -// If an invalid code was submitted with this flow more than 5 times, an error is returned -// TODO: Extract the business logic to a new service/manager (https://github.com/ory/kratos/issues/2785) -func (p *Persister) UseRecoveryCode(ctx context.Context, fID uuid.UUID, codeVal string) (*code.RecoveryCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRecoveryCode") - defer span.End() - - var recoveryCode *code.RecoveryCode - - nid := p.NetworkID(ctx) - - flowTableName := new(recovery.Flow).TableName(ctx) - - if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { - - //#nosec G201 -- TableName is static - if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), fID, nid).Exec()); err != nil { - return err - } - - var submitCount int - // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. - //#nosec G201 -- TableName is static - if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), fID, nid).First(&submitCount)); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - - // This check prevents parallel brute force attacks to generate the recovery code - // by checking the submit count inside this database transaction. - // If the flow has been submitted more than 5 times, the transaction is aborted (regardless of whether the code was correct or not) - // and we thus give no indication whether the supplied code was correct or not. See also https://github.com/ory/kratos/pull/2645#discussion_r984732899 - if submitCount > 5 { - return errors.WithStack(code.ErrCodeSubmittedTooOften) - } - - var recoveryCodes []code.RecoveryCode - if err = sqlcon.HandleError(tx.Where("nid = ? AND selfservice_recovery_flow_id = ?", nid, fID).All(&recoveryCodes)); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - - secrets: - for _, secret := range p.r.Config().SecretsSession(ctx) { - suppliedCode := []byte(p.hmacValueWithSecret(ctx, codeVal, secret)) - for i := range recoveryCodes { - code := recoveryCodes[i] - if subtle.ConstantTimeCompare([]byte(code.CodeHMAC), suppliedCode) == 0 { - // Not the supplied code - continue - } - recoveryCode = &code - break secrets - } - } - - if recoveryCode == nil || !recoveryCode.IsValid() { - // Return no error, as that would roll back the transaction - return nil - } - - var ra identity.RecoveryAddress - if err := tx.Where("id = ? AND nid = ?", recoveryCode.RecoveryAddressID, nid).First(&ra); err != nil { - if err = sqlcon.HandleError(err); !errors.Is(err, sqlcon.ErrNoRows) { - return err - } - } - recoveryCode.RecoveryAddress = &ra - - //#nosec G201 -- TableName is static - return sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", recoveryCode.TableName(ctx)), time.Now().UTC(), recoveryCode.ID, nid).Exec()) - })); err != nil { - return nil, err - } - - if recoveryCode == nil { - return nil, code.ErrCodeNotFound - } - - if recoveryCode.IsExpired() { - return nil, flow.NewFlowExpiredError(recoveryCode.ExpiresAt) - } - - if recoveryCode.WasUsed() { - return nil, code.ErrCodeAlreadyUsed - } - - return recoveryCode, nil -} - -func (p *Persister) DeleteRecoveryCodesOfFlow(ctx context.Context, fID uuid.UUID) error { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRecoveryCodesOfFlow") - defer span.End() - - //#nosec G201 -- TableName is static - return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE selfservice_recovery_flow_id = ? AND nid = ?", new(code.RecoveryCode).TableName(ctx)), fID, p.NetworkID(ctx)).Exec() -} diff --git a/persistence/sql/persister_recovery_code.go b/persistence/sql/persister_recovery_code.go new file mode 100644 index 000000000000..725b9578a205 --- /dev/null +++ b/persistence/sql/persister_recovery_code.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateRecoveryCode(ctx context.Context, params *code.CreateRecoveryCodeParams) (*code.RecoveryCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryCode") + defer span.End() + + now := time.Now() + recoveryCode := &code.RecoveryCode{ + ID: uuid.Nil, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + ExpiresAt: now.UTC().Add(params.ExpiresIn), + IssuedAt: now, + CodeType: params.CodeType, + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + IdentityID: params.IdentityID, + } + + if params.RecoveryAddress != nil { + recoveryCode.RecoveryAddress = params.RecoveryAddress + recoveryCode.RecoveryAddressID = uuid.NullUUID{ + UUID: params.RecoveryAddress.ID, + Valid: true, + } + } + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(recoveryCode); err != nil { + return nil, err + } + + return recoveryCode, nil +} + +// UseRecoveryCode attempts to "use" the supplied code in the flow +// +// If the supplied code matched a code from the flow, no error is returned +// If an invalid code was submitted with this flow more than 5 times, an error is returned +func (p *Persister) UseRecoveryCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string) (*code.RecoveryCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRecoveryCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.RecoveryCode, *code.RecoveryCode](ctx, p, flowID, userProvidedCode, new(recovery.Flow).TableName(ctx), "selfservice_recovery_flow_id") + if err != nil { + return nil, err + } + + var ra identity.RecoveryAddress + if err := sqlcon.HandleError(p.GetConnection(ctx).Where("id = ? AND nid = ?", codeRow.RecoveryAddressID, p.NetworkID(ctx)).First(&ra)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // This is ok, it can happen when an administrator initiates account recovery. This works even if the + // user has no recovery address! + } else { + return nil, err + } + } + codeRow.RecoveryAddress = &ra + + return codeRow, nil +} + +func (p *Persister) DeleteRecoveryCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRecoveryCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_recovery_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.RecoveryCode{}) +} diff --git a/persistence/sql/persister_registration_code.go b/persistence/sql/persister_registration_code.go new file mode 100644 index 000000000000..5c9ac909838c --- /dev/null +++ b/persistence/sql/persister_registration_code.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/bxcodec/faker/v3/support/slice" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateRegistrationCode(ctx context.Context, params *code.CreateRegistrationCodeParams) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRegistrationCode") + defer span.End() + + now := time.Now().UTC() + registrationCode := &code.RegistrationCode{ + Address: params.Address, + AddressType: params.AddressType, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(registrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return registrationCode, nil +} + +func (p *Persister) UseRegistrationCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string, addresses ...string) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRegistrationCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.RegistrationCode, *code.RegistrationCode](ctx, p, flowID, userProvidedCode, new(registration.Flow).TableName(ctx), "selfservice_registration_flow_id") + if err != nil { + return nil, err + } + + // ensure that the identifiers extracted from the traits are contained in the registration code + if !slice.Contains(addresses, codeRow.Address) { + return nil, errors.WithStack(code.ErrCodeNotFound) + } + + return codeRow, nil +} + +func (p *Persister) GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedRegistrationCode") + defer span.End() + + var registrationCode code.RegistrationCode + if err := p.Connection(ctx).Where("selfservice_registration_flow_id = ? AND used_at IS NOT NULL AND nid = ?", flowID, p.NetworkID(ctx)).First(®istrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return ®istrationCode, nil +} + +func (p *Persister) DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRegistrationCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_registration_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.RegistrationCode{}) +} diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go index 30bc4ac56718..b2f19f94726b 100644 --- a/persistence/sql/persister_verification.go +++ b/persistence/sql/persister_verification.go @@ -5,13 +5,11 @@ package sql import ( "context" - "crypto/subtle" "fmt" "time" "github.com/pkg/errors" - "github.com/ory/herodot" "github.com/ory/kratos/identity" "github.com/ory/kratos/persistence/sql/update" @@ -21,7 +19,6 @@ import ( "github.com/ory/x/sqlcon" "github.com/ory/kratos/selfservice/flow/verification" - "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/selfservice/strategy/link" ) @@ -137,154 +134,3 @@ func (p *Persister) DeleteExpiredVerificationFlows(ctx context.Context, expiresA } return nil } -func (p *Persister) UseVerificationCode(ctx context.Context, fID uuid.UUID, codeVal string) (*code.VerificationCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseVerificationCode") - defer span.End() - - var verificationCode *code.VerificationCode - - nid := p.NetworkID(ctx) - - flowTableName := new(verification.Flow).TableName(ctx) - - if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { - - if err := sqlcon.HandleError( - tx.RawQuery( - //#nosec G201 -- TableName is static - fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), - fID, - nid, - ).Exec(), - ); err != nil { - return err - } - - var submitCount int - // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. - if err := sqlcon.HandleError( - tx.RawQuery( - //#nosec G201 -- TableName is static - fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), - fID, - nid, - ).First(&submitCount), - ); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - // This check prevents parallel brute force attacks to generate the verification code - // by checking the submit count inside this database transaction. - // If the flow has been submitted more than 5 times, the transaction is aborted (regardless of whether the code was correct or not) - // and we thus give no indication whether the supplied code was correct or not. See also https://github.com/ory/kratos/pull/2645#discussion_r984732899 - if submitCount > 5 { - return errors.WithStack(code.ErrCodeSubmittedTooOften) - } - - var verificationCodes []code.VerificationCode - if err = sqlcon.HandleError( - tx.Where("nid = ? AND selfservice_verification_flow_id = ?", nid, fID). - All(&verificationCodes), - ); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - - secrets: - for _, secret := range p.r.Config().SecretsSession(ctx) { - suppliedCode := []byte(p.hmacValueWithSecret(ctx, codeVal, secret)) - for i := range verificationCodes { - code := verificationCodes[i] - if subtle.ConstantTimeCompare([]byte(code.CodeHMAC), suppliedCode) == 0 { - // Not the supplied code - continue - } - verificationCode = &code - break secrets - } - } - - if verificationCode == nil || verificationCode.Validate() != nil { - // Return no error, as that would roll back the transaction - return nil - } - - var va identity.VerifiableAddress - if err := tx.Where("id = ? AND nid = ?", verificationCode.VerifiableAddressID, nid).First(&va); err != nil { - return sqlcon.HandleError(err) - } - - verificationCode.VerifiableAddress = &va - - //#nosec G201 -- TableName is static - return tx. - RawQuery( - fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", verificationCode.TableName(ctx)), - time.Now().UTC(), - verificationCode.ID, - nid, - ).Exec() - })); err != nil { - return nil, err - } - - if verificationCode == nil { - return nil, code.ErrCodeNotFound - } - - return verificationCode, nil -} - -func (p *Persister) CreateVerificationCode(ctx context.Context, c *code.CreateVerificationCodeParams) (*code.VerificationCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateVerificationCode") - defer span.End() - - now := time.Now().UTC() - - verificationCode := &code.VerificationCode{ - ID: uuid.Nil, - CodeHMAC: p.hmacValue(ctx, c.RawCode), - ExpiresAt: now.Add(c.ExpiresIn), - IssuedAt: now, - FlowID: c.FlowID, - NID: p.NetworkID(ctx), - } - - if c.VerifiableAddress == nil { - return nil, errors.WithStack(herodot.ErrNotFound.WithReason("can't create a verification code without a verifiable address")) - } - - verificationCode.VerifiableAddress = c.VerifiableAddress - verificationCode.VerifiableAddressID = uuid.NullUUID{ - UUID: c.VerifiableAddress.ID, - Valid: true, - } - - // This should not create the request eagerly because otherwise we might accidentally create an address that isn't - // supposed to be in the database. - if err := p.GetConnection(ctx).Create(verificationCode); err != nil { - return nil, err - } - return verificationCode, nil -} - -func (p *Persister) DeleteVerificationCodesOfFlow(ctx context.Context, fID uuid.UUID) error { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteVerificationCodesOfFlow") - defer span.End() - - //#nosec G201 -- TableName is static - return p.GetConnection(ctx). - RawQuery( - fmt.Sprintf("DELETE FROM %s WHERE selfservice_verification_flow_id = ? AND nid = ?", new(code.VerificationCode).TableName(ctx)), - fID, - p.NetworkID(ctx), - ).Exec() -} diff --git a/persistence/sql/persister_verification_code.go b/persistence/sql/persister_verification_code.go new file mode 100644 index 000000000000..0b469bad07ce --- /dev/null +++ b/persistence/sql/persister_verification_code.go @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateVerificationCode(ctx context.Context, params *code.CreateVerificationCodeParams) (*code.VerificationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateVerificationCode") + defer span.End() + + now := time.Now().UTC() + verificationCode := &code.VerificationCode{ + ID: uuid.Nil, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + ExpiresAt: now.Add(params.ExpiresIn), + IssuedAt: now, + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + } + + if params.VerifiableAddress == nil { + return nil, errors.WithStack(herodot.ErrNotFound.WithReason("can't create a verification code without a verifiable address")) + } + + verificationCode.VerifiableAddress = params.VerifiableAddress + verificationCode.VerifiableAddressID = uuid.NullUUID{ + UUID: params.VerifiableAddress.ID, + Valid: true, + } + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(verificationCode); err != nil { + return nil, err + } + + return verificationCode, nil +} + +func (p *Persister) UseVerificationCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string) (*code.VerificationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseVerificationCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.VerificationCode, *code.VerificationCode](ctx, p, flowID, userProvidedCode, new(verification.Flow).TableName(ctx), "selfservice_verification_flow_id") + if err != nil { + return nil, err + } + + var va identity.VerifiableAddress + if err := p.Connection(ctx).Where("id = ? AND nid = ?", codeRow.VerifiableAddressID, p.NetworkID(ctx)).First(&va); err != nil { + // This should fail on not found errors too, because the verifiable address must exist for the flow to work. + return nil, sqlcon.HandleError(err) + } + codeRow.VerifiableAddress = &va + + return codeRow, nil +} + +func (p *Persister) DeleteVerificationCodesOfFlow(ctx context.Context, fID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteVerificationCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_verification_flow_id = ? AND nid = ?", fID, p.NetworkID(ctx)).Delete(&code.VerificationCode{}) +} diff --git a/schema/errors.go b/schema/errors.go index 73d144e0be9e..8cd74c5aec60 100644 --- a/schema/errors.go +++ b/schema/errors.go @@ -346,3 +346,43 @@ func NewNoWebAuthnCredentials() error { Messages: new(text.Messages).Add(text.NewErrorValidationSuchNoWebAuthnUser()), }) } + +func NewNoCodeAuthnCredentials() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `account does not exist or has not setup up sign in with code`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationNoCodeUser()), + }) +} + +func NewTraitsMismatch() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the submitted form data has changed from the previous submission`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationTraitsMismatch()), + }) +} + +func NewRegistrationCodeInvalid() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided code is invalid or has already been used`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed()), + }) +} + +func NewLoginCodeInvalid() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided code is invalid or has already been used`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed()), + }) +} diff --git a/schema/extension.go b/schema/extension.go index 5955328c27df..5b605cfb91d4 100644 --- a/schema/extension.go +++ b/schema/extension.go @@ -30,6 +30,10 @@ type ( TOTP struct { AccountName bool `json:"account_name"` } `json:"totp"` + Code struct { + Identifier bool `json:"identifier"` + Via string `json:"via"` + } `json:"code"` } `json:"credentials"` Verification struct { Via string `json:"via"` diff --git a/selfservice/flow/error_test.go b/selfservice/flow/error_test.go index 6502244fba69..98b1ad32e9c4 100644 --- a/selfservice/flow/error_test.go +++ b/selfservice/flow/error_test.go @@ -52,6 +52,17 @@ type testFlow struct { // // required: true UI *container.Container `json:"ui" db:"ui"` + + // Flow State + // + // The state represents the state of the verification flow. + // + // - choose_method: ask the user to choose a method (e.g. recover account via email) + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the recovery challenge was passed. + // + // required: true + State State `json:"state" db:"state"` } func (t *testFlow) GetID() uuid.UUID { @@ -74,6 +85,18 @@ func (t *testFlow) GetUI() *container.Container { return t.UI } +func (t *testFlow) GetState() State { + return t.State +} + +func (t *testFlow) GetFlowName() FlowName { + return FlowName("test") +} + +func (t *testFlow) SetState(state State) { + t.State = state +} + func newTestFlow(r *http.Request, flowType Type) Flow { id := x.NewUUID() requestURL := x.RequestURL(r).String() diff --git a/selfservice/flow/flow.go b/selfservice/flow/flow.go index c581b117ac04..be486e3723ae 100644 --- a/selfservice/flow/flow.go +++ b/selfservice/flow/flow.go @@ -38,6 +38,9 @@ type Flow interface { GetRequestURL() string AppendTo(*url.URL) *url.URL GetUI() *container.Container + GetState() State + SetState(State) + GetFlowName() FlowName } type FlowWithRedirect interface { diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index a0276175e2e4..e46698290819 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -124,8 +124,19 @@ type Flow struct { // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", // and only on creating the login flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // State represents the state of this request: + // + // - choose_method: ask the user to choose a method to sign in with + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the login challenge was passed. + // + // required: true + State State `json:"state" faker:"-" db:"state"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, flowType flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -164,6 +175,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques r.URL.Query().Get("aal"), string(identity.AuthenticatorAssuranceLevel1)))), InternalContext: []byte("{}"), + State: flow.StateChooseMethod, }, nil } @@ -251,3 +263,15 @@ func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (o x.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowLoginReturnTo(ctx, f.Active.String())), } } + +func (f *Flow) GetState() flow.State { + return flow.State(f.State) +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.LoginFlow +} + +func (f *Flow) SetState(state flow.State) { + f.State = State(state) +} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index c04b8cd0b2e3..203a62fdf989 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -142,7 +142,6 @@ func (e *HookExecutor) PostLoginHook( x.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())), x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(r.Context(), a.Active.String())), ) - if err != nil { return err } diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go index 74160b8d2566..9f1a144ffc2d 100644 --- a/selfservice/flow/login/sort.go +++ b/selfservice/flow/login/sort.go @@ -15,6 +15,7 @@ func sortNodes(ctx context.Context, n node.Nodes) error { node.OpenIDConnectGroup, node.DefaultGroup, node.WebAuthnGroup, + node.CodeGroup, node.PasswordGroup, node.TOTPGroup, node.LookupGroup, diff --git a/selfservice/flow/login/state.go b/selfservice/flow/login/state.go new file mode 100644 index 000000000000..576fad6d9f05 --- /dev/null +++ b/selfservice/flow/login/state.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import "github.com/ory/kratos/selfservice/flow" + +// Login Flow State +// +// The state represents the state of the login flow. +// +// - choose_method: ask the user to choose a method (e.g. login account via email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the login challenge was passed. +// +// swagger:model loginFlowState +type State = flow.State diff --git a/selfservice/flow/name.go b/selfservice/flow/name.go new file mode 100644 index 000000000000..1b766c6662f6 --- /dev/null +++ b/selfservice/flow/name.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +// FlowName is the flow name. +// +// The flow name can be one of: +// - 'login' +// - 'registration' +// - 'settings' +// - 'recovery' +// - 'verification' +// +// swagger:ignore +type FlowName string + +const ( + LoginFlow FlowName = "login" + RegistrationFlow FlowName = "registration" + SettingsFlow FlowName = "settings" + RecoveryFlow FlowName = "recovery" + VerificationFlow FlowName = "verification" +) + +func (t Type) String() string { + return string(t) +} diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go index 42770783fb42..3557c8652b8d 100644 --- a/selfservice/flow/recovery/flow.go +++ b/selfservice/flow/recovery/flow.go @@ -102,6 +102,8 @@ type Flow struct { DangerousSkipCSRFCheck bool `json:"-" faker:"-" db:"skip_csrf_check"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, strategy Strategy, ft flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -127,13 +129,13 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques Method: "POST", Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), }, - State: StateChooseMethod, + State: flow.StateChooseMethod, CSRFToken: csrf, Type: ft, } if strategy != nil { - flow.Active = sqlxx.NullString(strategy.RecoveryNodeGroup()) + flow.Active = sqlxx.NullString(strategy.NodeGroup()) if err := strategy.PopulateRecoveryMethod(r, flow); err != nil { return nil, err } @@ -222,3 +224,15 @@ func (f *Flow) AfterSave(*pop.Connection) error { func (f *Flow) GetUI() *container.Container { return f.UI } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.RecoveryFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/recovery/flow_test.go b/selfservice/flow/recovery/flow_test.go index c2a1eb56e11d..cab497c1b9a2 100644 --- a/selfservice/flow/recovery/flow_test.go +++ b/selfservice/flow/recovery/flow_test.go @@ -54,7 +54,7 @@ func TestFlow(t *testing.T) { }) } - assert.EqualValues(t, recovery.StateChooseMethod, + assert.EqualValues(t, flow.StateChooseMethod, must(recovery.NewFlow(conf, time.Hour, "", u, nil, flow.TypeBrowser)).State) t.Run("type=return_to", func(t *testing.T) { diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go index 24aed7695bba..6c4fd21344a4 100644 --- a/selfservice/flow/recovery/handler.go +++ b/selfservice/flow/recovery/handler.go @@ -185,7 +185,6 @@ type createBrowserRecoveryFlow struct { // 400: errorGeneric // default: errorGeneric func (h *Handler) createBrowserRecoveryFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - if !h.d.Config().SelfServiceFlowRecoveryEnabled(r.Context()) { h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Recovery is not allowed because it was disabled."))) return @@ -430,12 +429,12 @@ func (h *Handler) updateRecoveryFlow(w http.ResponseWriter, r *http.Request, ps } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { - h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, ss.RecoveryNodeGroup(), err) + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } found = true - g = ss.RecoveryNodeGroup() + g = ss.NodeGroup() break } diff --git a/selfservice/flow/recovery/state.go b/selfservice/flow/recovery/state.go index 6b2ed0892b06..96ab9d29937e 100644 --- a/selfservice/flow/recovery/state.go +++ b/selfservice/flow/recovery/state.go @@ -3,6 +3,8 @@ package recovery +import "github.com/ory/kratos/selfservice/flow" + // Recovery Flow State // // The state represents the state of the recovery flow. @@ -12,33 +14,4 @@ package recovery // - passed_challenge: the request was successful and the recovery challenge was passed. // // swagger:model recoveryFlowState -type State string - -const ( - StateChooseMethod State = "choose_method" - StateEmailSent State = "sent_email" - StatePassedChallenge State = "passed_challenge" -) - -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} - -func indexOf(current State) int { - for k, s := range states { - if s == current { - return k - } - } - return 0 -} - -func HasReachedState(expected, actual State) bool { - return indexOf(actual) >= indexOf(expected) -} - -func NextState(current State) State { - if current == StatePassedChallenge { - return StatePassedChallenge - } - - return states[indexOf(current)+1] -} +type State = flow.State diff --git a/selfservice/flow/recovery/strategy.go b/selfservice/flow/recovery/strategy.go index 28e28673f1d3..3594420167df 100644 --- a/selfservice/flow/recovery/strategy.go +++ b/selfservice/flow/recovery/strategy.go @@ -26,7 +26,7 @@ const ( type ( Strategy interface { RecoveryStrategyID() string - RecoveryNodeGroup() node.UiNodeGroup + NodeGroup() node.UiNodeGroup PopulateRecoveryMethod(*http.Request, *Flow) error Recover(w http.ResponseWriter, r *http.Request, f *Flow) (err error) } diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go index 91e8a2c763b3..665dc84b9130 100644 --- a/selfservice/flow/recovery/test/persistence.go +++ b/selfservice/flow/recovery/test/persistence.go @@ -15,6 +15,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" @@ -24,8 +25,9 @@ import ( func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { - var clearids = func(r *recovery.Flow) { +}, +) func(t *testing.T) { + clearids := func(r *recovery.Flow) { r.ID = uuid.UUID{} } @@ -38,10 +40,11 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { require.Error(t, err) }) - var newFlow = func(t *testing.T) *recovery.Flow { + newFlow := func(t *testing.T) *recovery.Flow { var r recovery.Flow require.NoError(t, faker.FakeData(&r)) clearids(&r) + r.State = flow.StateShowForm return &r } @@ -61,6 +64,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { t.Run("case=should create with set ids", func(t *testing.T) { var r recovery.Flow require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateShowForm require.NoError(t, p.CreateRecoveryFlow(ctx, &r)) require.Equal(t, nid, r.NID) @@ -162,7 +166,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("case=handle network reference issues", func(t *testing.T) { - }) } } diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index b16bf2048ce5..39843a9e5b5c 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -115,8 +115,18 @@ type Flow struct { // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", // and only on creating the flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // State represents the state of this request: + // + // - choose_method: ask the user to choose a method (e.g. registration with email) + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the registration challenge was passed. + // required: true + State State `json:"state" faker:"-" db:"state"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, ft flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -151,6 +161,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques CSRFToken: csrf, Type: ft, InternalContext: []byte("{}"), + State: flow.StateChooseMethod, }, nil } @@ -238,3 +249,15 @@ func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (o x.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowRegistrationReturnTo(ctx, f.Active.String())), } } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.RegistrationFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/registration/sort.go b/selfservice/flow/registration/sort.go index db44e96274e3..15348dd56512 100644 --- a/selfservice/flow/registration/sort.go +++ b/selfservice/flow/registration/sort.go @@ -16,6 +16,7 @@ func SortNodes(ctx context.Context, n node.Nodes, schemaRef string) error { node.DefaultGroup, node.OpenIDConnectGroup, node.WebAuthnGroup, + node.CodeGroup, node.PasswordGroup, }), node.SortUpdateOrder(node.PasswordLoginOrder), diff --git a/selfservice/flow/registration/state.go b/selfservice/flow/registration/state.go new file mode 100644 index 000000000000..be50551d6850 --- /dev/null +++ b/selfservice/flow/registration/state.go @@ -0,0 +1,15 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import "github.com/ory/kratos/selfservice/flow" + +// State represents the state of this request: +// +// - choose_method: ask the user to choose a method (e.g. registration with email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the registration challenge was passed. +// +// swagger:model registrationFlowState +type State = flow.State diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go index af1b31968caa..6c7bc9709525 100644 --- a/selfservice/flow/request.go +++ b/selfservice/flow/request.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/strategy" "github.com/ory/x/decoderx" @@ -25,6 +26,7 @@ var methodSchema []byte var ErrOriginHeaderNeedsBrowserFlow = herodot.ErrBadRequest. WithReasonf(`The HTTP Request Header included the "Origin" key, indicating that this request was made as part of an AJAX request in a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) + var ErrCookieHeaderNeedsBrowserFlow = herodot.ErrBadRequest. WithReasonf(`The HTTP Request Header included the "Cookie" key, indicating that this request was made by a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) @@ -76,9 +78,10 @@ func EnsureCSRF(reg interface { var dec = decoderx.NewHTTP() -func MethodEnabledAndAllowedFromRequest(r *http.Request, expected string, d interface { +func MethodEnabledAndAllowedFromRequest(r *http.Request, flow FlowName, expected string, d interface { config.Provider -}) error { +}, +) error { var method struct { Method string `json:"method" form:"method"` } @@ -96,17 +99,27 @@ func MethodEnabledAndAllowedFromRequest(r *http.Request, expected string, d inte return errors.WithStack(err) } - return MethodEnabledAndAllowed(r.Context(), expected, method.Method, d) + return MethodEnabledAndAllowed(r.Context(), flow, expected, method.Method, d) } -func MethodEnabledAndAllowed(ctx context.Context, expected, actual string, d interface { - config.Provider -}) error { +func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, actual string, d config.Provider) error { if actual != expected { return errors.WithStack(ErrStrategyNotResponsible) } - if !d.Config().SelfServiceStrategy(ctx, expected).Enabled { + var ok bool + if strings.EqualFold(actual, identity.CredentialsTypeCodeAuth.String()) { + switch flowName { + case RegistrationFlow, LoginFlow: + ok = d.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled + case VerificationFlow, RecoveryFlow: + ok = d.Config().SelfServiceCodeStrategy(ctx).Enabled + } + } else { + ok = d.Config().SelfServiceStrategy(ctx, expected).Enabled + } + + if !ok { return errors.WithStack(herodot.ErrNotFound.WithReason(strategy.EndpointDisabledMessage)) } diff --git a/selfservice/flow/request_test.go b/selfservice/flow/request_test.go index d240f9c15daf..17a7d9e2d0d3 100644 --- a/selfservice/flow/request_test.go +++ b/selfservice/flow/request_test.go @@ -55,7 +55,7 @@ func TestMethodEnabledAndAllowed(t *testing.T) { ctx := context.Background() conf, d := internal.NewFastRegistryWithMocks(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if err := flow.MethodEnabledAndAllowedFromRequest(r, "password", d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, flow.LoginFlow, "password", d); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -91,3 +91,74 @@ func TestMethodEnabledAndAllowed(t *testing.T) { assert.Contains(t, string(body), "The requested resource could not be found") }) } + +func TestMethodCodeEnabledAndAllowed(t *testing.T) { + ctx := context.Background() + conf, d := internal.NewFastRegistryWithMocks(t) + + currentFlow := make(chan flow.FlowName, 1) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f := <-currentFlow + if err := flow.MethodEnabledAndAllowedFromRequest(r, f, "code", d); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + })) + + t.Run("login code allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + currentFlow <- flow.LoginFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + }) + + t.Run("login code not allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false) + currentFlow <- flow.LoginFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "The requested resource could not be found") + }) + + t.Run("registration code allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + currentFlow <- flow.RegistrationFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + }) + + t.Run("registration code not allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false) + currentFlow <- flow.RegistrationFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "The requested resource could not be found") + }) + + t.Run("recovery and verification should still be allowed if registration and login is disabled", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + + for _, f := range []flow.FlowName{flow.RecoveryFlow, flow.VerificationFlow} { + currentFlow <- f + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + } + }) +} diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go index ef69a03f042e..a96da053d766 100644 --- a/selfservice/flow/settings/flow.go +++ b/selfservice/flow/settings/flow.go @@ -121,6 +121,8 @@ type Flow struct { ContinueWithItems []flow.ContinueWith `json:"continue_with,omitempty" db:"-" faker:"-" ` } +var _ flow.Flow = new(Flow) + func MustNewFlow(conf *config.Config, exp time.Duration, r *http.Request, i *identity.Identity, ft flow.Type) *Flow { f, err := NewFlow(conf, exp, r, i, ft) if err != nil { @@ -153,7 +155,7 @@ func NewFlow(conf *config.Config, exp time.Duration, r *http.Request, i *identit IdentityID: i.ID, Identity: i, Type: ft, - State: StateShowForm, + State: flow.StateShowForm, UI: &container.Container{ Method: "POST", Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), @@ -242,3 +244,15 @@ func (f *Flow) AddContinueWith(c flow.ContinueWith) { func (f *Flow) ContinueWith() []flow.ContinueWith { return f.ContinueWithItems } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.SettingsFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go index 91c6b2c5122c..c5e35610c7a2 100644 --- a/selfservice/flow/settings/hook.go +++ b/selfservice/flow/settings/hook.go @@ -231,7 +231,7 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, Debug("An identity's settings have been updated.") ctxUpdate.UpdateIdentity(i) - ctxUpdate.Flow.State = StateSuccess + ctxUpdate.Flow.State = flow.StateSuccess if hookOptions.cb != nil { if err := hookOptions.cb(ctxUpdate); err != nil { return err diff --git a/selfservice/flow/settings/state.go b/selfservice/flow/settings/state.go index b605cf7569d8..21cd22f303cc 100644 --- a/selfservice/flow/settings/state.go +++ b/selfservice/flow/settings/state.go @@ -3,6 +3,8 @@ package settings +import "github.com/ory/kratos/selfservice/flow" + // State represents the state of this flow. It knows two states: // // - show_form: No user data has been collected, or it is invalid, and thus the form should be shown. @@ -11,9 +13,4 @@ package settings // when a flow with invalid (e.g. "please use a valid phone number") data was sent. // // swagger:model settingsFlowState -type State string - -const ( - StateShowForm State = "show_form" - StateSuccess State = "success" -) +type State = flow.State diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go index 82b56f74d6e5..62bce9c547bf 100644 --- a/selfservice/flow/settings/test/persistence.go +++ b/selfservice/flow/settings/test/persistence.go @@ -14,6 +14,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/x/sqlcon" @@ -54,6 +55,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P require.NoError(t, p.CreateIdentity(ctx, r.Identity)) require.NotZero(t, r.Identity.ID) r.IdentityID = r.Identity.ID + r.State = flow.StateShowForm return &r } @@ -106,6 +108,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P t.Run("case=should create with set ids", func(t *testing.T) { var r settings.Flow require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateShowForm require.NoError(t, p.CreateIdentity(ctx, r.Identity)) require.NoError(t, p.CreateSettingsFlow(ctx, &r)) @@ -146,6 +149,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P clearids(&expected) expected.Identity = nil expected.IdentityID = uuid.Nil + expected.State = flow.StateShowForm err := p.CreateSettingsFlow(ctx, &expected) require.Errorf(t, err, "%+v", expected) }) @@ -201,7 +205,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P require.NoError(t, p.CreateIdentity(ctx, &identity.Identity{ID: iid})) t.Run("sets id on creation", func(t *testing.T) { - expected := &settings.Flow{ID: id, IdentityID: iid, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)} + expected := &settings.Flow{ID: id, IdentityID: iid, State: flow.StateShowForm, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)} require.NoError(t, p.CreateSettingsFlow(ctx, expected)) assert.EqualValues(t, id, expected.ID) assert.EqualValues(t, nid, expected.NID) diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go new file mode 100644 index 000000000000..76a0683fc19d --- /dev/null +++ b/selfservice/flow/state.go @@ -0,0 +1,100 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + + "github.com/pkg/errors" +) + +// Flow State +// +// The state represents the state of the verification flow. +// +// - choose_method: ask the user to choose a method (e.g. recover account via email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the recovery challenge was passed. +// - show_form: a form is shown to the user to perform the flow +// - success: the flow has been completed successfully +// +// swagger:enum selfServiceFlowState +type State string + +// #nosec G101 -- only a key constant +const ( + StateChooseMethod State = "choose_method" + StateEmailSent State = "sent_email" + StatePassedChallenge State = "passed_challenge" + StateShowForm State = "show_form" + StateSuccess State = "success" +) + +var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} + +func indexOf(current State) int { + for k, s := range states { + if s == current { + return k + } + } + return 0 +} + +func HasReachedState(expected, actual State) bool { + return indexOf(actual) >= indexOf(expected) +} + +func NextState(current State) State { + if current == StatePassedChallenge { + return StatePassedChallenge + } + + return states[indexOf(current)+1] +} + +// For some reason using sqlxx.NullString as the State type does not work here. +// Reimplementing the Scanner interface on type State does work and allows +// the state to be NULL in the database. + +// MarshalJSON returns m as the JSON encoding of m. +func (ns State) MarshalJSON() ([]byte, error) { + return json.Marshal(string(ns)) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *State) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal(data, (*string)(ns))) +} + +// Scan implements the Scanner interface. +func (ns *State) Scan(value interface{}) error { + var v sql.NullString + if err := (&v).Scan(value); err != nil { + return err + } + *ns = State(v.String) + return nil +} + +// Value implements the driver Valuer interface. +func (ns State) Value() (driver.Value, error) { + if len(ns) == 0 { + return sql.NullString{}.Value() + } + return sql.NullString{Valid: true, String: string(ns)}.Value() +} + +// String implements the Stringer interface. +func (ns State) String() string { + return string(ns) +} diff --git a/selfservice/flow/recovery/state_test.go b/selfservice/flow/state_test.go similarity index 97% rename from selfservice/flow/recovery/state_test.go rename to selfservice/flow/state_test.go index 160be6e74555..349e0425ed44 100644 --- a/selfservice/flow/recovery/state_test.go +++ b/selfservice/flow/state_test.go @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -package recovery +package flow import ( "testing" diff --git a/selfservice/flow/verification/error.go b/selfservice/flow/verification/error.go index fd7542415fb1..b39875b233d0 100644 --- a/selfservice/flow/verification/error.go +++ b/selfservice/flow/verification/error.go @@ -26,9 +26,7 @@ import ( "github.com/ory/kratos/x" ) -var ( - ErrHookAbortFlow = errors.New("aborted verification hook execution") -) +var ErrHookAbortFlow = errors.New("aborted verification hook execution") type ( errorHandlerDependencies interface { diff --git a/selfservice/flow/verification/fake_strategy.go b/selfservice/flow/verification/fake_strategy.go index 58f6b1e5af5c..d497fb5111f3 100644 --- a/selfservice/flow/verification/fake_strategy.go +++ b/selfservice/flow/verification/fake_strategy.go @@ -13,6 +13,8 @@ import ( type FakeStrategy struct{} +var _ Strategy = new(FakeStrategy) + func (f FakeStrategy) VerificationStrategyID() string { return "fake" } @@ -32,3 +34,7 @@ func (f FakeStrategy) Verify(_ http.ResponseWriter, _ *http.Request, _ *Flow) (e func (f FakeStrategy) SendVerificationEmail(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress) error { return nil } + +func (f FakeStrategy) NodeGroup() node.UiNodeGroup { + return "fake" +} diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go index 71a484f7d9c5..28a71e47a977 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -106,6 +106,8 @@ type OAuth2LoginChallengeParams struct { AMR session.AuthenticationMethods `db:"authentication_methods" json:"-"` } +var _ flow.Flow = new(Flow) + func (f *Flow) GetType() flow.Type { return f.Type } @@ -144,12 +146,12 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), }, CSRFToken: csrf, - State: StateChooseMethod, + State: flow.StateChooseMethod, Type: ft, } if strategy != nil { - f.Active = sqlxx.NullString(strategy.VerificationNodeGroup()) + f.Active = sqlxx.NullString(strategy.NodeGroup()) if err := strategy.PopulateVerificationMethod(r, f); err != nil { return nil, err } @@ -270,3 +272,15 @@ func (f *Flow) ContinueURL(ctx context.Context, config *config.Config) *url.URL } return returnTo } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.VerificationFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/verification/flow_test.go b/selfservice/flow/verification/flow_test.go index fb21b707e387..3485f3454fc4 100644 --- a/selfservice/flow/verification/flow_test.go +++ b/selfservice/flow/verification/flow_test.go @@ -64,7 +64,7 @@ func TestFlow(t *testing.T) { require.NoError(t, err) }) - assert.EqualValues(t, verification.StateChooseMethod, + assert.EqualValues(t, flow.StateChooseMethod, must(verification.NewFlow(conf, time.Hour, "", u, nil, flow.TypeBrowser)).State) } @@ -207,5 +207,4 @@ func TestContinueURL(t *testing.T) { require.Equal(t, tc.expect, url.String()) }) } - } diff --git a/selfservice/flow/verification/handler.go b/selfservice/flow/verification/handler.go index 977cb7bd8616..83a21418e83b 100644 --- a/selfservice/flow/verification/handler.go +++ b/selfservice/flow/verification/handler.go @@ -427,12 +427,12 @@ func (h *Handler) updateVerificationFlow(w http.ResponseWriter, r *http.Request, } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { - h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.VerificationNodeGroup(), err) + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } found = true - g = ss.VerificationNodeGroup() + g = ss.NodeGroup() break } diff --git a/selfservice/flow/verification/handler_test.go b/selfservice/flow/verification/handler_test.go index 092f659e4a36..6eb2b12f800b 100644 --- a/selfservice/flow/verification/handler_test.go +++ b/selfservice/flow/verification/handler_test.go @@ -23,6 +23,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/session" "github.com/ory/kratos/x" @@ -203,6 +204,7 @@ func TestPostFlow(t *testing.T) { Type: "browser", ExpiresAt: time.Now().Add(1 * time.Hour), IssuedAt: time.Now(), + State: flow.StateChooseMethod, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) @@ -227,6 +229,7 @@ func TestPostFlow(t *testing.T) { IdentityID: uuid.NullUUID{UUID: uuid.Must(uuid.NewV4()), Valid: true}, AMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, }, + State: flow.StatePassedChallenge, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) @@ -254,6 +257,7 @@ func TestPostFlow(t *testing.T) { ExpiresAt: time.Now().Add(1 * time.Hour), IssuedAt: time.Now(), OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + State: flow.StateChooseMethod, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) @@ -264,5 +268,4 @@ func TestPostFlow(t *testing.T) { assert.Equal(t, f.ID.String(), resp.Request.URL.Query().Get("flow")) }) }) - } diff --git a/selfservice/flow/verification/state.go b/selfservice/flow/verification/state.go index 9f11037cfeef..84aded971389 100644 --- a/selfservice/flow/verification/state.go +++ b/selfservice/flow/verification/state.go @@ -3,6 +3,8 @@ package verification +import "github.com/ory/kratos/selfservice/flow" + // Verification Flow State // // The state represents the state of the verification flow. @@ -12,33 +14,4 @@ package verification // - passed_challenge: the request was successful and the recovery challenge was passed. // // swagger:model verificationFlowState -type State string - -const ( - StateChooseMethod State = "choose_method" - StateEmailSent State = "sent_email" - StatePassedChallenge State = "passed_challenge" -) - -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} - -func indexOf(current State) int { - for k, s := range states { - if s == current { - return k - } - } - return 0 -} - -func HasReachedState(expected, actual State) bool { - return indexOf(actual) >= indexOf(expected) -} - -func NextState(current State) State { - if current == StatePassedChallenge { - return StatePassedChallenge - } - - return states[indexOf(current)+1] -} +type State = flow.State diff --git a/selfservice/flow/verification/state_test.go b/selfservice/flow/verification/state_test.go deleted file mode 100644 index ab192d4db878..000000000000 --- a/selfservice/flow/verification/state_test.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package verification - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestState(t *testing.T) { - assert.EqualValues(t, StateEmailSent, NextState(StateChooseMethod)) - assert.EqualValues(t, StatePassedChallenge, NextState(StateEmailSent)) - assert.EqualValues(t, StatePassedChallenge, NextState(StatePassedChallenge)) - - assert.True(t, HasReachedState(StatePassedChallenge, StatePassedChallenge)) - assert.False(t, HasReachedState(StatePassedChallenge, StateEmailSent)) - assert.False(t, HasReachedState(StateEmailSent, StateChooseMethod)) -} diff --git a/selfservice/flow/verification/strategy.go b/selfservice/flow/verification/strategy.go index 9cb4ff848920..3d270bfb8732 100644 --- a/selfservice/flow/verification/strategy.go +++ b/selfservice/flow/verification/strategy.go @@ -26,7 +26,7 @@ const ( type ( Strategy interface { VerificationStrategyID() string - VerificationNodeGroup() node.UiNodeGroup + NodeGroup() node.UiNodeGroup PopulateVerificationMethod(*http.Request, *Flow) error Verify(w http.ResponseWriter, r *http.Request, f *Flow) (err error) SendVerificationEmail(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress) error diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go index dac7f539e334..0358462029f7 100644 --- a/selfservice/flow/verification/test/persistence.go +++ b/selfservice/flow/verification/test/persistence.go @@ -15,6 +15,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" @@ -24,8 +25,9 @@ import ( func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { - var clearids = func(r *verification.Flow) { +}, +) func(t *testing.T) { + clearids := func(r *verification.Flow) { r.ID = uuid.UUID{} } @@ -39,10 +41,11 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { require.Error(t, err) }) - var newFlow = func(t *testing.T) *verification.Flow { + newFlow := func(t *testing.T) *verification.Flow { var r verification.Flow require.NoError(t, faker.FakeData(&r)) clearids(&r) + r.State = flow.StateChooseMethod return &r } @@ -62,6 +65,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { t.Run("case=should create with set ids", func(t *testing.T) { var r verification.Flow require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateChooseMethod require.NoError(t, p.CreateVerificationFlow(ctx, &r)) require.Equal(t, nid, r.NID) diff --git a/selfservice/hook/code_address_verifier.go b/selfservice/hook/code_address_verifier.go new file mode 100644 index 000000000000..b222970cb9af --- /dev/null +++ b/selfservice/hook/code_address_verifier.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "net/http" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" +) + +type ( + codeAddressDependencies interface { + code.RegistrationCodePersistenceProvider + } + CodeAddressVerifier struct { + r codeAddressDependencies + } +) + +var _ registration.PostHookPrePersistExecutor = new(CodeAddressVerifier) + +func NewCodeAddressVerifier(r codeAddressDependencies) *CodeAddressVerifier { + return &CodeAddressVerifier{r: r} +} + +func (cv *CodeAddressVerifier) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, i *identity.Identity) error { + if a.Active != identity.CredentialsTypeCodeAuth { + return nil + } + + recoveryCode, err := cv.r.RegistrationCodePersister().GetUsedRegistrationCode(r.Context(), a.GetID()) + if err != nil { + return err + } + + if recoveryCode == nil { + return nil + } + + for idx := range i.VerifiableAddresses { + va := &i.VerifiableAddresses[idx] + if !va.Verified && recoveryCode.Address == va.Value { + va.Verified = true + va.Status = identity.VerifiableAddressStatusCompleted + break + } + } + + return nil +} diff --git a/selfservice/hook/code_address_verifier_test.go b/selfservice/hook/code_address_verifier_test.go new file mode 100644 index 000000000000..579432e60556 --- /dev/null +++ b/selfservice/hook/code_address_verifier_test.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "net/http" + "strings" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/randx" +) + +func TestCodeAddressVerifier(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.schema.json") + verifier := hook.NewCodeAddressVerifier(reg) + + setup := func(t *testing.T) (address string, rf *registration.Flow) { + t.Helper() + address = testhelpers.RandomEmail() + rawCode := strings.ToLower(randx.MustString(16, randx.Alpha)) + + rf = ®istration.Flow{Active: identity.CredentialsTypeCodeAuth, Type: "browser", State: flow.StatePassedChallenge} + require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(ctx, rf)) + + _, err := reg.RegistrationCodePersister().CreateRegistrationCode(ctx, &code.CreateRegistrationCodeParams{ + Address: address, + AddressType: identity.CodeAddressTypeEmail, + RawCode: rawCode, + ExpiresIn: time.Hour, + FlowID: rf.ID, + }) + require.NoError(t, err) + + _, err = reg.RegistrationCodePersister().UseRegistrationCode(ctx, rf.ID, rawCode, address) + require.NoError(t, err) + + return + } + + setupIdentity := func(t *testing.T, address string) *identity.Identity { + t.Helper() + verifiableAddress := []identity.VerifiableAddress{{ID: uuid.UUID{}, Verified: false, Value: address, Via: identity.VerifiableAddressTypeEmail}} + id := &identity.Identity{ID: x.NewUUID(), VerifiableAddresses: verifiableAddress, Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: {Type: identity.CredentialsTypeCodeAuth, Identifiers: []string{address}}, + }} + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, id)) + return id + } + + runHook := func(t *testing.T, id *identity.Identity, flow *registration.Flow) { + t.Helper() + + r := &http.Request{} + require.NoError(t, verifier.ExecutePostRegistrationPrePersistHook(nil, r, flow, id)) + } + + t.Run("case=should set the verifiable email address to verified", func(t *testing.T) { + address, flow := setup(t) + id := setupIdentity(t, address) + require.False(t, id.VerifiableAddresses[0].Verified) + runHook(t, id, flow) + require.True(t, id.VerifiableAddresses[0].Verified) + }) + + t.Run("case=should ignore verifiable email address that does not match the code", func(t *testing.T) { + _, flow := setup(t) + newEmail := testhelpers.RandomEmail() + id := setupIdentity(t, newEmail) + require.False(t, id.VerifiableAddresses[0].Verified) + runHook(t, id, flow) + require.False(t, id.VerifiableAddresses[0].Verified) + }) +} diff --git a/selfservice/hook/stub/code.schema.json b/selfservice/hook/stub/code.schema.json new file mode 100644 index 000000000000..71219c0b9db4 --- /dev/null +++ b/selfservice/hook/stub/code.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } + } + } +} diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index ba4d7e7d79a7..e72f833faabf 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -20,8 +20,10 @@ import ( "github.com/ory/x/otelx" ) -var _ registration.PostHookPostPersistExecutor = new(Verifier) -var _ settings.PostHookPostPersistExecutor = new(Verifier) +var ( + _ registration.PostHookPostPersistExecutor = new(Verifier) + _ settings.PostHookPostPersistExecutor = new(Verifier) +) type ( verifierDependencies interface { @@ -100,7 +102,7 @@ func (e *Verifier) do( flowCallback(verificationFlow) } - verificationFlow.State = verification.StateEmailSent + verificationFlow.State = flow.StateEmailSent if err := strategy.PopulateVerificationMethod(r, verificationFlow); err != nil { return err diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 1013de192223..3d4195b6e0ba 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -22,7 +22,6 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" - "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" "github.com/ory/kratos/x" @@ -94,7 +93,7 @@ func TestVerifier(t *testing.T) { expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) require.NoError(t, err) - require.Equal(t, expectedVerificationFlow.State, verification.StateEmailSent) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) require.NoError(t, err) @@ -110,7 +109,7 @@ func TestVerifier(t *testing.T) { // Email to baz@ory.sh is skipped because it is verified already. assert.NotContains(t, recipients, "baz@ory.sh") - //these addresses will be marked as sent and won't be sent again by the settings hook + // these addresses will be marked as sent and won't be sent again by the settings hook address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") require.NoError(t, err) assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json new file mode 100644 index 000000000000..fa030cbd67a0 --- /dev/null +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/code/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "code" + ] + }, + "code": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "resend": { + "type": "string", + "enum": [ + "code" + ] + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "csrf_token": { + "type": "string" + } + } +} diff --git a/selfservice/strategy/code/.schema/registration.schema.json b/selfservice/strategy/code/.schema/registration.schema.json new file mode 100644 index 000000000000..90f245c107c5 --- /dev/null +++ b/selfservice/strategy/code/.schema/registration.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/code/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "code" + ] + }, + "csrf_token": { + "type": "string" + }, + "code": { + "type": "string" + }, + "resend": { + "type": "string", + "enum": [ + "code" + ] + }, + "traits": { + "description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect." + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index a1993da4d3d0..42456da54dc5 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -1,4 +1,17 @@ [ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, { "type": "input", "group": "code", @@ -18,19 +31,6 @@ } } }, - { - "type": "input", - "group": "code", - "attributes": { - "name": "method", - "type": "hidden", - "value": "code", - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": {} - }, { "type": "input", "group": "code", diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go new file mode 100644 index 000000000000..689d52f0cb4f --- /dev/null +++ b/selfservice/strategy/code/code_login.go @@ -0,0 +1,114 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "time" + + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" +) + +// swagger:ignore +type LoginCode struct { + // ID represents the tokens's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Address represents the address that the code was sent to. + // this can be an email address or a phone number. + Address string `json:"-" db:"address"` + + // AddressType represents the type of the address + // this can be an email address or a phone number. + AddressType identity.CodeAddressType `json:"-" db:"address_type"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_login_flow_id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + IdentityID uuid.UUID `json:"identity_id" faker:"-" db:"identity_id"` +} + +func (LoginCode) TableName(ctx context.Context) string { + return "identity_login_codes" +} + +func (f *LoginCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } + if f.ExpiresAt.Before(time.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) + } + if f.UsedAt.Valid { + return errors.WithStack(ErrCodeAlreadyUsed) + } + return nil +} + +func (f *LoginCode) GetHMACCode() string { + return f.CodeHMAC +} + +func (f *LoginCode) GetID() uuid.UUID { + return f.ID +} + +// swagger:ignore +type CreateLoginCodeParams struct { + // Address is the email address or phone number the code should be sent to. + // required: true + Address string + + // AddressType is the type of the address (email or phone number). + // required: true + AddressType identity.CodeAddressType + + // Code represents the recovery code + // required: true + RawCode string + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // FlowID is a helper struct field for gobuffalo.pop. + // required: true + FlowID uuid.UUID + + // IdentityID is the identity that this code is for + // required: true + IdentityID uuid.UUID +} diff --git a/selfservice/strategy/code/code_login_test.go b/selfservice/strategy/code/code_login_test.go new file mode 100644 index 000000000000..87b50155a15b --- /dev/null +++ b/selfservice/strategy/code/code_login_test.go @@ -0,0 +1,81 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestLoginCode(t *testing.T) { + conf, _ := internal.NewFastRegistryWithMocks(t) + + newCode := func(expiresIn time.Duration, f *login.Flow) *code.LoginCode { + return &code.LoginCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if flow is expired", func(t *testing.T) { + f, err := login.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := login.NewFlow(conf, time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := login.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := login.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.LoginCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) + }) + }) +} diff --git a/selfservice/strategy/code/code_recovery.go b/selfservice/strategy/code/code_recovery.go index f87a9c398de7..8d0cbe926063 100644 --- a/selfservice/strategy/code/code_recovery.go +++ b/selfservice/strategy/code/code_recovery.go @@ -8,6 +8,10 @@ import ( "database/sql" "time" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow" + "github.com/gofrs/uuid" "github.com/ory/herodot" @@ -73,16 +77,25 @@ func (RecoveryCode) TableName(ctx context.Context) string { return "identity_recovery_codes" } -func (f RecoveryCode) IsExpired() bool { - return f.ExpiresAt.Before(time.Now()) +func (f *RecoveryCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } + if f.ExpiresAt.Before(time.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) + } + if f.UsedAt.Valid { + return errors.WithStack(ErrCodeAlreadyUsed) + } + return nil } -func (r RecoveryCode) WasUsed() bool { - return r.UsedAt.Valid +func (f *RecoveryCode) GetHMACCode() string { + return f.CodeHMAC } -func (f RecoveryCode) IsValid() bool { - return !f.IsExpired() && !f.WasUsed() +func (f *RecoveryCode) GetID() uuid.UUID { + return f.ID } type CreateRecoveryCodeParams struct { diff --git a/selfservice/strategy/code/code_recovery_test.go b/selfservice/strategy/code/code_recovery_test.go index 3aadf350bb8c..dc099f02cae0 100644 --- a/selfservice/strategy/code/code_recovery_test.go +++ b/selfservice/strategy/code/code_recovery_test.go @@ -34,26 +34,26 @@ func TestRecoveryCode(t *testing.T) { } req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() - t.Run("method=IsExpired", func(t *testing.T) { - t.Run("case=returns true if flow is expired", func(t *testing.T) { + t.Run("case=returns error if flow is expired", func(t *testing.T) { f, err := recovery.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) c := newCode(-time.Hour, f) - require.True(t, c.IsExpired()) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) }) - t.Run("case=returns false if flow is not expired", func(t *testing.T) { + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { f, err := recovery.NewFlow(conf, time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) c := newCode(time.Hour, f) - require.False(t, c.IsExpired()) + require.NoError(t, c.Validate()) }) - }) - t.Run("method=WasUsed", func(t *testing.T) { - t.Run("case=returns true if flow has been used", func(t *testing.T) { + t.Run("case=returns error if flow has been used", func(t *testing.T) { f, err := recovery.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) @@ -62,9 +62,10 @@ func TestRecoveryCode(t *testing.T) { Time: time.Now(), Valid: true, } - require.True(t, c.WasUsed()) + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) }) - t.Run("case=returns false if flow has not been used", func(t *testing.T) { + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { f, err := recovery.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) @@ -72,7 +73,12 @@ func TestRecoveryCode(t *testing.T) { c.UsedAt = sql.NullTime{ Valid: false, } - require.False(t, c.WasUsed()) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.RecoveryCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) }) }) } diff --git a/selfservice/strategy/code/code_registration.go b/selfservice/strategy/code/code_registration.go new file mode 100644 index 000000000000..d8691b54b224 --- /dev/null +++ b/selfservice/strategy/code/code_registration.go @@ -0,0 +1,109 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "time" + + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" +) + +// swagger:ignore +type RegistrationCode struct { + // ID represents the tokens's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Address represents the address that the code was sent to. + // this can be an email address or a phone number. + Address string `json:"-" db:"address"` + + // AddressType represents the type of the address + // this can be an email address or a phone number. + AddressType identity.CodeAddressType `json:"-" db:"address_type"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_registration_flow_id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (RegistrationCode) TableName(ctx context.Context) string { + return "identity_registration_codes" +} + +func (f *RegistrationCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } + if f.ExpiresAt.Before(time.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) + } + if f.UsedAt.Valid { + return errors.WithStack(ErrCodeAlreadyUsed) + } + return nil +} + +func (f *RegistrationCode) GetHMACCode() string { + return f.CodeHMAC +} + +func (f *RegistrationCode) GetID() uuid.UUID { + return f.ID +} + +// swagger:ignore +type CreateRegistrationCodeParams struct { + // Address is the email address or phone number the code should be sent to. + // required: true + Address string + + // AddressType is the type of the address (email or phone number). + // required: true + AddressType identity.CodeAddressType + + // Code represents the recovery code + // required: true + RawCode string + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // FlowID is a helper struct field for gobuffalo.pop. + // required: true + FlowID uuid.UUID +} diff --git a/selfservice/strategy/code/code_registration_test.go b/selfservice/strategy/code/code_registration_test.go new file mode 100644 index 000000000000..034d9fcf2b92 --- /dev/null +++ b/selfservice/strategy/code/code_registration_test.go @@ -0,0 +1,80 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestRegistrationCode(t *testing.T) { + conf, _ := internal.NewFastRegistryWithMocks(t) + newCode := func(expiresIn time.Duration, f *registration.Flow) *code.RegistrationCode { + return &code.RegistrationCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if flow is expired", func(t *testing.T) { + f, err := registration.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := registration.NewFlow(conf, time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := registration.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := registration.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.RegistrationCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) + }) + }) +} diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index 5f48437131b5..d3667aea3b07 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -22,6 +22,7 @@ import ( "github.com/ory/kratos/courier" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/x" @@ -40,6 +41,8 @@ type ( RecoveryCodePersistenceProvider VerificationCodePersistenceProvider + RegistrationCodePersistenceProvider + LoginCodePersistenceProvider HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client } @@ -50,6 +53,10 @@ type ( Sender struct { deps senderDependencies } + Address struct { + To string + Via identity.CodeAddressType + } ) var ErrUnknownAddress = herodot.ErrNotFound.WithReason("recovery requested for unknown address") @@ -58,6 +65,97 @@ func NewSender(deps senderDependencies) *Sender { return &Sender{deps: deps} } +func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identity, addresses ...Address) error { + s.deps.Logger(). + WithSensitiveField("address", addresses). + Debugf("Preparing %s code", f.GetFlowName()) + + // send to all addresses + for _, address := range addresses { + // We have to generate a unique code per address, or otherwise it is not possible to link which + // address was used to verify the code. + // + // See also [this discussion](https://github.com/ory/kratos/pull/3456#discussion_r1307560988). + rawCode := GenerateCode() + + switch f.GetFlowName() { + case flow.RegistrationFlow: + code, err := s.deps. + RegistrationCodePersister(). + CreateRegistrationCode(ctx, &CreateRegistrationCodeParams{ + AddressType: address.Via, + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + FlowID: f.GetID(), + Address: address.To, + }) + if err != nil { + return err + } + model, err := x.StructToMap(id.Traits) + if err != nil { + return err + } + + emailModel := email.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Traits: model, + } + + s.deps.Audit(). + WithField("registration_flow_id", code.FlowID). + WithField("registration_code_id", code.ID). + WithSensitiveField("registration_code", rawCode). + Info("Sending out registration email with code.") + + if err := s.send(ctx, string(address.Via), email.NewRegistrationCodeValid(s.deps, &emailModel)); err != nil { + return errors.WithStack(err) + } + + case flow.LoginFlow: + code, err := s.deps. + LoginCodePersister(). + CreateLoginCode(ctx, &CreateLoginCodeParams{ + AddressType: address.Via, + Address: address.To, + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + FlowID: f.GetID(), + IdentityID: id.ID, + }) + if err != nil { + return err + } + + model, err := x.StructToMap(id) + if err != nil { + return err + } + + emailModel := email.LoginCodeValidModel{ + To: address.To, + LoginCode: rawCode, + Identity: model, + } + s.deps.Audit(). + WithField("login_flow_id", code.FlowID). + WithField("login_code_id", code.ID). + WithSensitiveField("login_code", rawCode). + Info("Sending out login email with code.") + + if err := s.send(ctx, string(address.Via), email.NewLoginCodeValid(s.deps, &emailModel)); err != nil { + return errors.WithStack(err) + } + + default: + return errors.WithStack(errors.New("received unknown flow type")) + + } + } + return nil +} + // SendRecoveryCode sends a recovery code to the specified address // // If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is diff --git a/selfservice/strategy/code/code_sender_test.go b/selfservice/strategy/code/code_sender_test.go index 6b74bf4f1c53..e5ba75826eb5 100644 --- a/selfservice/strategy/code/code_sender_test.go +++ b/selfservice/strategy/code/code_sender_test.go @@ -48,7 +48,6 @@ func TestSender(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(ctx, i)) t.Run("method=SendRecoveryCode", func(t *testing.T) { - recoveryCode := func(t *testing.T) { t.Helper() f, err := recovery.NewFlow(conf, time.Hour, "", u, code.NewStrategy(reg), flow.TypeBrowser) @@ -101,7 +100,6 @@ func TestSender(t *testing.T) { assert.Equal(t, messages[1].Subject, subject+" invalid") assert.Equal(t, messages[1].Body, body) }) - }) t.Run("method=SendVerificationCode", func(t *testing.T) { @@ -198,7 +196,6 @@ func TestSender(t *testing.T) { }, } { t.Run("strategy="+tc.flow, func(t *testing.T) { - conf.Set(ctx, tc.configKey, false) t.Cleanup(func() { @@ -214,5 +211,4 @@ func TestSender(t *testing.T) { }) } }) - } diff --git a/selfservice/strategy/code/verification_code.go b/selfservice/strategy/code/code_verification.go similarity index 93% rename from selfservice/strategy/code/verification_code.go rename to selfservice/strategy/code/code_verification.go index a4f204bae176..324766ebfa1c 100644 --- a/selfservice/strategy/code/verification_code.go +++ b/selfservice/strategy/code/code_verification.go @@ -62,6 +62,9 @@ func (VerificationCode) TableName(context.Context) string { // - If the code was already used `ErrCodeAlreadyUsed` is returnd // - Otherwise, `nil` is returned func (f *VerificationCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } if f.ExpiresAt.Before(time.Now().UTC()) { return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) } @@ -71,6 +74,14 @@ func (f *VerificationCode) Validate() error { return nil } +func (f *VerificationCode) GetHMACCode() string { + return f.CodeHMAC +} + +func (f *VerificationCode) GetID() uuid.UUID { + return f.ID +} + type CreateVerificationCodeParams struct { // Code represents the recovery code RawCode string diff --git a/selfservice/strategy/code/code_verification_test.go b/selfservice/strategy/code/code_verification_test.go new file mode 100644 index 000000000000..3217b7dcbb00 --- /dev/null +++ b/selfservice/strategy/code/code_verification_test.go @@ -0,0 +1,81 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestVerificationCode(t *testing.T) { + conf, _ := internal.NewFastRegistryWithMocks(t) + + newCode := func(expiresIn time.Duration, f *verification.Flow) *code.VerificationCode { + return &code.VerificationCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if flow is expired", func(t *testing.T) { + f, err := verification.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := verification.NewFlow(conf, time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := verification.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := verification.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.VerificationCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) + }) + }) +} diff --git a/selfservice/strategy/code/persistence.go b/selfservice/strategy/code/persistence.go index b64a6bbfb0c8..ea5aaff682cc 100644 --- a/selfservice/strategy/code/persistence.go +++ b/selfservice/strategy/code/persistence.go @@ -29,4 +29,26 @@ type ( VerificationCodePersistenceProvider interface { VerificationCodePersister() VerificationCodePersister } + + RegistrationCodePersistenceProvider interface { + RegistrationCodePersister() RegistrationCodePersister + } + + RegistrationCodePersister interface { + CreateRegistrationCode(context.Context, *CreateRegistrationCodeParams) (*RegistrationCode, error) + UseRegistrationCode(ctx context.Context, flowID uuid.UUID, code string, addresses ...string) (*RegistrationCode, error) + DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*RegistrationCode, error) + } + + LoginCodePersistenceProvider interface { + LoginCodePersister() LoginCodePersister + } + + LoginCodePersister interface { + CreateLoginCode(context.Context, *CreateLoginCodeParams) (*LoginCode, error) + UseLoginCode(ctx context.Context, flowID uuid.UUID, identityID uuid.UUID, code string) (*LoginCode, error) + DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + GetUsedLoginCode(ctx context.Context, flowID uuid.UUID) (*LoginCode, error) + } ) diff --git a/selfservice/strategy/code/schema.go b/selfservice/strategy/code/schema.go index 69d5bd07393e..d3ec2c66cf81 100644 --- a/selfservice/strategy/code/schema.go +++ b/selfservice/strategy/code/schema.go @@ -12,3 +12,9 @@ var recoveryMethodSchema []byte //go:embed .schema/verification.schema.json var verificationMethodSchema []byte + +//go:embed .schema/login.schema.json +var loginMethodSchema []byte + +//go:embed .schema/registration.schema.json +var registrationSchema []byte diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index b35d8bda30ff..94c8de75e9b6 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -4,29 +4,51 @@ package code import ( + "net/http" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" "github.com/ory/kratos/courier" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/session" + "github.com/ory/kratos/text" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/decoderx" "github.com/ory/x/randx" + "github.com/ory/x/urlx" ) -var _ recovery.Strategy = new(Strategy) -var _ recovery.AdminHandler = new(Strategy) -var _ recovery.PublicHandler = new(Strategy) +var ( + _ recovery.Strategy = new(Strategy) + _ recovery.AdminHandler = new(Strategy) + _ recovery.PublicHandler = new(Strategy) +) -var _ verification.Strategy = new(Strategy) -var _ verification.AdminHandler = new(Strategy) -var _ verification.PublicHandler = new(Strategy) +var ( + _ verification.Strategy = new(Strategy) + _ verification.AdminHandler = new(Strategy) + _ verification.PublicHandler = new(Strategy) +) + +var ( + _ login.Strategy = new(Strategy) + _ registration.Strategy = new(Strategy) +) type ( // FlowMethod contains the configuration for this selfservice strategy. @@ -39,6 +61,7 @@ type ( x.CSRFTokenGeneratorProvider x.WriterProvider x.LoggingProvider + x.TracingProvider config.Provider @@ -65,31 +88,250 @@ type ( verification.StrategyProvider verification.HookExecutorProvider + login.StrategyProvider + login.FlowPersistenceProvider + + registration.StrategyProvider + registration.FlowPersistenceProvider + RecoveryCodePersistenceProvider VerificationCodePersistenceProvider SenderProvider + RegistrationCodePersistenceProvider + LoginCodePersistenceProvider + schema.IdentityTraitsProvider + + sessiontokenexchange.PersistenceProvider + + continuity.ManagementProvider } Strategy struct { deps strategyDependencies dx *decoderx.HTTP } + + codeIdentifier struct { + Identifier string `json:"identifier"` + } ) func NewStrategy(deps strategyDependencies) *Strategy { return &Strategy{deps: deps, dx: decoderx.NewHTTP()} } -func (s *Strategy) RecoveryNodeGroup() node.UiNodeGroup { - return node.CodeGroup +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeCodeAuth } -func (s *Strategy) VerificationNodeGroup() node.UiNodeGroup { +func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.CodeGroup } +func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { + if string(f.GetState()) == "" { + f.SetState(flow.StateChooseMethod) + } + + f.GetUI().ResetMessages() + + nodes := f.GetUI().Nodes + + switch f.GetState() { + case flow.StateChooseMethod: + + if f.GetFlowName() == flow.VerificationFlow || f.GetFlowName() == flow.RecoveryFlow { + nodes.Append( + node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + } else if f.GetFlowName() == flow.LoginFlow { + // we use the identifier label here since we don't know what + // type of field the identifier is + nodes.Upsert( + node.NewInputField("identifier", nil, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeLabelID()), + ) + } else if f.GetFlowName() == flow.RegistrationFlow { + ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + // set the traits on the default group so that the ui can render them + // this prevents having multiple of the same ui fields on the same ui form + traitNodes, err := container.NodesFromJSONSchema(r.Context(), node.CodeGroup, ds.String(), "", nil) + if err != nil { + return err + } + + for _, n := range traitNodes { + nodes.Append(n) + } + } + + var codeMetaLabel *text.Message + + switch f.GetFlowName() { + case flow.VerificationFlow, flow.RecoveryFlow: + codeMetaLabel = text.NewInfoNodeLabelSubmit() + case flow.LoginFlow: + codeMetaLabel = text.NewInfoSelfServiceLoginCode() + case flow.RegistrationFlow: + codeMetaLabel = text.NewInfoSelfServiceRegistrationRegisterCode() + } + + methodButton := node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(codeMetaLabel) + + nodes.Append(methodButton) + + f.GetUI().Nodes = nodes + + case flow.StateEmailSent: + // fresh ui node group + freshNodes := node.Nodes{} + var route string + var codeMetaLabel *text.Message + var message *text.Message + + var resendNode *node.Node + + switch f.GetFlowName() { + case flow.RecoveryFlow: + route = recovery.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelRecoveryCode() + message = text.NewRecoveryEmailWithCodeSent() + + resendNode = node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeResendOTP()) + case flow.VerificationFlow: + route = verification.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelVerificationCode() + message = text.NewVerificationEmailWithCodeSent() + + case flow.LoginFlow: + route = login.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelLoginCode() + message = text.NewLoginEmailWithCodeSent() + + // preserve the login identifier that were submitted + // so we can retry the code flow with the same data + for _, n := range f.GetUI().Nodes { + if n.Group == node.DefaultGroup { + freshNodes = append(freshNodes, n) + } + } + + resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()) + + case flow.RegistrationFlow: + route = registration.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelRegistrationCode() + message = text.NewRegistrationEmailWithCodeSent() + + // in the registration flow we need to preserve the trait fields that were submitted + // so we can retry the code flow with the same data + for _, n := range f.GetUI().Nodes { + if t, ok := n.Attributes.(*node.InputAttributes); ok && t.Type == node.InputAttributeTypeSubmit { + continue + } + + if n.Group == node.CodeGroup { + freshNodes = append(freshNodes, n) + } + } + + resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()) + default: + return errors.WithStack(herodot.ErrBadRequest.WithReason("received an unexpected flow type")) + } + + // Hidden field Required for the re-send code button + // !!important!!: this field must be appended before the code submit button since upsert will replace the first node with the same name + freshNodes.Upsert( + node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), + ) + + // code input field + freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + WithMetaLabel(codeMetaLabel)) + + // code submit button + freshNodes. + Append(node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelSubmit())) + + if resendNode != nil { + freshNodes.Append(resendNode) + } + + f.GetUI().Nodes = freshNodes + + f.GetUI().Method = "POST" + f.GetUI().Action = flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), route), f.GetID()).String() + + // Set the request's CSRF token + if f.GetType() == flow.TypeBrowser { + f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r)) + } + + f.GetUI().Messages.Set(message) + + case flow.StatePassedChallenge: + fallthrough + default: + return errors.WithStack(herodot.ErrBadRequest.WithReason("received an unexpected flow state")) + } + + // no matter the flow type or state we need to set the CSRF token + if f.GetType() == flow.TypeBrowser { + f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r)) + } + return nil +} + +// NewCodeUINodes creates a fresh UI for the code flow. +// this is used with the `recovery`, `verification`, `registration` and `login` flows. +func (s *Strategy) NewCodeUINodes(r *http.Request, f flow.Flow, data any) error { + if err := s.PopulateMethod(r, f); err != nil { + return err + } + + prefix := "" // The login flow does not process traits + if f.GetFlowName() == flow.RegistrationFlow { + // The registration form does however + prefix = "traits" + } + + cont, err := container.NewFromStruct("", node.CodeGroup, data, prefix) + if err != nil { + return err + } + + for _, n := range cont.Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.GetUI().GetNodes().SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + + return nil +} + +func SetDefaultFlowState(f flow.Flow, resend string) { + // By Default the flow should be in the 'choose method' state. + if f.GetState() == "" { + f.SetState(flow.StateChooseMethod) + } + + if strings.EqualFold(resend, "code") { + f.SetState(flow.StateChooseMethod) + } +} + const CodeLength = 6 func GenerateCode() string { diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go new file mode 100644 index 000000000000..264df98e2dd9 --- /dev/null +++ b/selfservice/strategy/code/strategy_login.go @@ -0,0 +1,279 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "net/http" + "strings" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/otelx" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +var _ login.Strategy = new(Strategy) + +// Update Login flow using the code method +// +// swagger:model updateLoginFlowWithCodeMethod +type updateLoginFlowWithCodeMethod struct { + // Method should be set to "code" when logging in using the code strategy. + // + // required: true + Method string `json:"method" form:"method"` + + // CSRFToken is the anti-CSRF token + // + // required: true + CSRFToken string `json:"csrf_token" form:"csrf_token"` + + // Code is the 6 digits code sent to the user + // + // required: false + Code string `json:"code" form:"code"` + + // Identifier is the code identifier + // The identifier requires that the user has already completed the registration or settings with code flow. + // required: false + Identifier string `json:"identifier" form:"identifier"` + + // Resend is set when the user wants to resend the code + // required: false + Resend string `json:"resend" form:"resend"` +} + +func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) {} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: identity.CredentialsTypeCodeAuth, + AAL: identity.AuthenticatorAssuranceLevel1, + } +} + +func (s *Strategy) HandleLoginError(r *http.Request, f *login.Flow, body *updateLoginFlowWithCodeMethod, err error) error { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } + + if f != nil { + email := "" + if body != nil { + email = body.Identifier + } + + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + f.UI.GetNodes().Upsert( + node.NewInputField("identifier", email, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeLabelID()), + ) + } + + return err +} + +func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, lf *login.Flow) error { + return s.PopulateMethod(r, lf) +} + +func (s *Strategy) getIdentity(ctx context.Context, identifier string) (_ *identity.Identity, _ *identity.Credentials, err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.getIdentity") + defer otelx.End(span, &err) + + i, cred, err := s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identifier) + if err != nil { + return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } + + if len(cred.Identifiers) == 0 { + return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } + + return i, cred, nil +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ uuid.UUID) (_ *identity.Identity, err error) { + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Login") + defer otelx.End(span, &err) + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { + return nil, err + } + + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, err + } + + var p updateLoginFlowWithCodeMethod + if err := s.dx.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPKeepRequestBody(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginMethodSchema), + decoderx.HTTPDecoderAllowedMethods("POST"), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.HandleLoginError(r, f, &p, err) + } + + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.HandleLoginError(r, f, &p, err) + } + + // By Default the flow should be in the 'choose method' state. + SetDefaultFlowState(f, p.Resend) + + switch f.GetState() { + case flow.StateChooseMethod: + if err := s.loginSendEmail(ctx, w, r, f, &p); err != nil { + return nil, s.HandleLoginError(r, f, &p, err) + } + return nil, nil + case flow.StateEmailSent: + i, err := s.loginVerifyCode(ctx, r, f, &p) + if err != nil { + return nil, s.HandleLoginError(r, f, &p, err) + } + return i, nil + case flow.StatePassedChallenge: + return nil, s.HandleLoginError(r, f, &p, errors.WithStack(schema.NewNoLoginStrategyResponsible())) + } + + return nil, s.HandleLoginError(r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unexpected flow state: %s", f.GetState()))) +} + +func (s *Strategy) loginSendEmail(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginSendEmail") + defer otelx.End(span, &err) + + if len(p.Identifier) == 0 { + return errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) + } + + p.Identifier = maybeNormalizeEmail(p.Identifier) + + // Step 1: Get the identity + i, _, err := s.getIdentity(ctx, p.Identifier) + if err != nil { + return err + } + + // Step 2: Delete any previous login codes for this flow ID + if err := s.deps.LoginCodePersister().DeleteLoginCodesOfFlow(ctx, f.GetID()); err != nil { + return errors.WithStack(err) + } + + addresses := []Address{{ + To: p.Identifier, + Via: identity.CodeAddressType(identity.AddressTypeEmail), + }} + + // kratos only supports `email` identifiers at the moment with the code method + // this is validated in the identity validation step above + if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { + return errors.WithStack(err) + } + + // sets the flow state to code sent + f.SetState(flow.NextState(f.GetState())) + + if err := s.NewCodeUINodes(r, f, &codeIdentifier{Identifier: p.Identifier}); err != nil { + return err + } + + f.Active = identity.CredentialsTypeCodeAuth + if err = s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return err + } + + if x.IsJSONRequest(r) { + s.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) + } + + // we return an error to the flow handler so that it does not continue execution of the hooks. + // we are not done with the login flow yet. The user needs to verify the code and then we need to persist the identity. + return errors.WithStack(flow.ErrCompletedByStrategy) +} + +// If identifier is an email, we lower case it because on mobile phones the first letter sometimes is capitalized. +func maybeNormalizeEmail(input string) string { + if strings.Contains(input, "@") { + return strings.ToLower(input) + } + return input +} + +func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod) (_ *identity.Identity, err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginVerifyCode") + defer otelx.End(span, &err) + + // we are in the second submission state of the flow + // we need to check the code and update the identity + if p.Code == "" { + return nil, errors.WithStack(schema.NewRequiredError("#/code", "code")) + } + + if len(p.Identifier) == 0 { + return nil, errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) + } + + p.Identifier = maybeNormalizeEmail(p.Identifier) + + // Step 1: Get the identity + i, _, err := s.getIdentity(ctx, p.Identifier) + if err != nil { + return nil, err + } + + loginCode, err := s.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) + if err != nil { + if errors.Is(err, ErrCodeNotFound) { + return nil, schema.NewLoginCodeInvalid() + } + return nil, errors.WithStack(err) + } + + i, err = s.deps.PrivilegedIdentityPool().GetIdentity(ctx, loginCode.IdentityID, identity.ExpandDefault) + if err != nil { + return nil, errors.WithStack(err) + } + + // Step 2: The code was correct + f.Active = identity.CredentialsTypeCodeAuth + + // since nothing has errored yet, we can assume that the code is correct + // and we can update the login flow + f.SetState(flow.NextState(f.GetState())) + + if err := s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, errors.WithStack(err) + } + + for idx := range i.VerifiableAddresses { + va := i.VerifiableAddresses[idx] + if !va.Verified && loginCode.Address == va.Value { + va.Verified = true + va.Status = identity.VerifiableAddressStatusCompleted + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, &va); err != nil { + return nil, err + } + break + } + } + + return i, nil +} diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go new file mode 100644 index 000000000000..a371fdfbf5f6 --- /dev/null +++ b/selfservice/strategy/code/strategy_login_test.go @@ -0,0 +1,456 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ory/x/stringsx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/session" + "github.com/ory/x/sqlxx" +) + +func TestLoginCodeStrategy(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) + + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + createIdentity := func(ctx context.Context, t *testing.T, moreIdentifiers ...string) *identity.Identity { + t.Helper() + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + email := testhelpers.RandomEmail() + + ids := fmt.Sprintf(`"email":"%s"`, email) + for i, identifier := range moreIdentifiers { + ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier) + } + + i.Traits = identity.Traits(fmt.Sprintf(`{%s}`, ids)) + + credentials := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, + identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")}, + identity.CredentialsTypeCodeAuth: {Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"address_type\": \"email\", \"used_at\": \"2023-07-26T16:59:06+02:00\"}")}, + } + i.Credentials = credentials + + var va []identity.VerifiableAddress + for _, identifier := range moreIdentifiers { + va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted}) + } + + va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted}) + + i.VerifiableAddresses = va + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + return i + } + + type state struct { + flowID string + identity *identity.Identity + client *http.Client + loginCode string + identityEmail string + testServer *httptest.Server + } + + createLoginFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, isSPA bool, moreIdentifiers ...string) *state { + t.Helper() + + identity := createIdentity(ctx, t, moreIdentifiers...) + + client := testhelpers.NewClientWithCookies(t) + client.Transport = testhelpers.NewTransportWithLogger(http.DefaultTransport, t).RoundTripper + clientInit := testhelpers.InitializeLoginFlowViaBrowser(t, client, public, false, isSPA, false, false) + + body, err := json.Marshal(clientInit) + require.NoError(t, err) + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmpty(t, csrfToken) + + loginEmail := gjson.Get(identity.Traits.String(), "email").String() + require.NotEmpty(t, loginEmail) + + return &state{ + flowID: clientInit.GetId(), + identity: identity, + identityEmail: loginEmail, + client: client, + testServer: public, + } + } + + type onSubmitAssertion func(t *testing.T, s *state, body string, res *http.Response) + + submitLogin := func(ctx context.Context, t *testing.T, s *state, isSPA bool, vals func(v *url.Values), mustHaveSession bool, submitAssertion onSubmitAssertion) *state { + t.Helper() + + lf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + values := testhelpers.SDKFormFieldsToURLValues(lf.Ui.Nodes) + // we need to remove resend here + // since it is not required for the first request + // subsequent requests might need it later + values.Del("resend") + values.Set("method", "code") + vals(&values) + + body, resp := testhelpers.LoginMakeRequest(t, false, isSPA, lf, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) + + if submitAssertion != nil { + submitAssertion(t, s, body, resp) + return s + } + + if mustHaveSession { + resp, err = s.client.Get(s.testServer.URL + session.RouteWhoami) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } else { + // SPAs need to be informed that the login has not yet completed using status 400. + // Browser clients will redirect back to the login URL. + if isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } + } + + return s + } + + for _, tc := range []struct { + d string + isSPA bool + }{ + { + d: "SPA client", + isSPA: true, + }, + { + d: "Browser client", + isSPA: false, + }, + } { + t.Run("test="+tc.d, func(t *testing.T) { + t.Run("case=email identifier should be case insensitive", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", stringsx.ToUpperInitial(s.identityEmail)) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + }) + + t.Run("case=should be able to log in with code", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + }) + + t.Run("case=should not be able to change submitted id on code submit", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", "not-"+s.identityEmail) + v.Set("code", loginCode) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + require.EqualValues(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) + + lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } + }) + }) + + t.Run("case=should not be able to proceed to code entry when the account is unknown", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", testhelpers.RandomEmail()) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + require.EqualValues(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) + + lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } + }) + }) + + t.Run("case=should not be able to use valid code after 5 attempts", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + for i := 0; i < 5; i++ { + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", "111111") + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + // in browser flows we redirect back to the login ui + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The login code is invalid or has already been used") + }) + } + + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + // in browser flows we redirect back to the login ui + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) + + t.Run("case=code should expire", func(t *testing.T) { + ctx := context.Background() + + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1ns") + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1h") + }) + + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusGone, resp.StatusCode) + require.Contains(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") + } else { + // with browser clients we redirect back to the UI with a new flow id as a query parameter + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) + lf, _, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "flow expired 0.00 minutes ago") + } + }) + }) + + t.Run("case=resend code should invalidate previous code", func(t *testing.T) { + ctx := context.Background() + + s := createLoginFlow(ctx, t, public, tc.isSPA) + + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // resend code + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("resend", "code") + v.Set("identifier", s.identityEmail) + }, false, nil) + + message = testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode2) + + assert.NotEqual(t, loginCode, loginCode2) + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, res *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, res.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, res.StatusCode) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The login code is invalid or has already been used. Please try again") + }) + + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode2) + v.Set("identifier", s.identityEmail) + }, true, nil) + }) + + t.Run("case=on login with un-verified address, should verify it", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.isSPA, testhelpers.RandomEmail()) + + // we need to fetch only the first email + loginEmail := gjson.Get(s.identity.Traits.String(), "email_1").String() + require.NotEmpty(t, loginEmail) + + s.identityEmail = loginEmail + + var va *identity.VerifiableAddress + + for _, v := range s.identity.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } + + require.NotNil(t, va) + require.False(t, va.Verified) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, loginEmail, "Login to your account") + require.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + require.NotEmpty(t, loginCode) + + // Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, true, nil) + + id, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, s.identity.ID, identity.ExpandEverything) + require.NoError(t, err) + + va = nil + + for _, v := range id.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } + + require.NotNil(t, va) + require.True(t, va.Verified) + }) + }) + } +} diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index d06c5e2c9009..e4b6fae22c55 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -177,23 +177,23 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. return } - flow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) + recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) if err != nil { s.deps.Writer().WriteError(w, r, err) return } - flow.DangerousSkipCSRFCheck = true - flow.State = recovery.StateEmailSent - flow.UI.Nodes = node.Nodes{} - flow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + recoveryFlow.DangerousSkipCSRFCheck = true + recoveryFlow.State = flow.StateEmailSent + recoveryFlow.UI.Nodes = node.Nodes{} + recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) - flow.UI.Nodes. + recoveryFlow.UI.Nodes. Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeLabelSubmit())) - if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, flow); err != nil { + if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil { s.deps.Writer().WriteError(w, r, err) return } @@ -213,7 +213,7 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. RawCode: rawCode, CodeType: RecoveryCodeTypeAdmin, ExpiresIn: expiresIn, - FlowID: flow.ID, + FlowID: recoveryFlow.ID, IdentityID: id.ID, }); err != nil { s.deps.Writer().WriteError(w, r, err) @@ -226,11 +226,11 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. Info("A recovery code has been created.") body := &recoveryCodeForIdentity{ - ExpiresAt: flow.ExpiresAt.UTC(), + ExpiresAt: recoveryFlow.ExpiresAt.UTC(), RecoveryLink: urlx.CopyWithQuery( s.deps.Config().SelfServiceFlowRecoveryUI(ctx), url.Values{ - "flow": {flow.ID.String()}, + "flow": {recoveryFlow.ID.String()}, }).String(), RecoveryCode: rawCode, } @@ -281,7 +281,7 @@ func (s Strategy) isCodeFlow(f *recovery.Flow) bool { if err != nil { return false } - return value == s.RecoveryNodeGroup().String() + return value == s.NodeGroup().String() } func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.Flow) (err error) { @@ -310,8 +310,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F f.UI.ResetMessages() // If the email is present in the submission body, the user needs a new code via resend - if f.State != recovery.StateChooseMethod && len(body.Email) == 0 { - if err := flow.MethodEnabledAndAllowed(ctx, sID, sID, s.deps); err != nil { + if f.State != flow.StateChooseMethod && len(body.Email) == 0 { + if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, sID, s.deps); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } return s.recoveryUseCode(w, r, body, f) @@ -327,29 +327,29 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return errors.WithStack(flow.ErrCompletedByStrategy) } - if err := flow.MethodEnabledAndAllowed(ctx, sID, body.Method, s.deps); err != nil { + if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, body.Method, s.deps); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } - flow, err := s.deps.RecoveryFlowPersister().GetRecoveryFlow(ctx, x.ParseUUID(body.Flow)) + recoveryFlow, err := s.deps.RecoveryFlowPersister().GetRecoveryFlow(ctx, x.ParseUUID(body.Flow)) if err != nil { - return s.HandleRecoveryError(w, r, flow, body, err) + return s.HandleRecoveryError(w, r, recoveryFlow, body, err) } - if err := flow.Valid(); err != nil { - return s.HandleRecoveryError(w, r, flow, body, err) + if err := recoveryFlow.Valid(); err != nil { + return s.HandleRecoveryError(w, r, recoveryFlow, body, err) } - switch flow.State { - case recovery.StateChooseMethod: + switch recoveryFlow.State { + case flow.StateChooseMethod: fallthrough - case recovery.StateEmailSent: - return s.recoveryHandleFormSubmission(w, r, flow, body) - case recovery.StatePassedChallenge: + case flow.StateEmailSent: + return s.recoveryHandleFormSubmission(w, r, recoveryFlow, body) + case flow.StatePassedChallenge: // was already handled, do not allow retry - return s.retryRecoveryFlowWithMessage(w, r, flow.Type, text.NewErrorValidationRecoveryRetrySuccess()) + return s.retryRecoveryFlowWithMessage(w, r, recoveryFlow.Type, text.NewErrorValidationRecoveryRetrySuccess()) default: - return s.retryRecoveryFlowWithMessage(w, r, flow.Type, text.NewErrorValidationRecoveryStateFailure()) + return s.retryRecoveryFlowWithMessage(w, r, recoveryFlow.Type, text.NewErrorValidationRecoveryStateFailure()) } } @@ -357,7 +357,7 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, ctx := r.Context() f.UI.Messages.Clear() - f.State = recovery.StatePassedChallenge + f.State = flow.StatePassedChallenge f.SetCSRFToken(s.deps.CSRFHandler().RegenerateToken(w, r)) f.RecoveredIdentityID = uuid.NullUUID{ UUID: id.ID, @@ -539,13 +539,13 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - f.Active = sqlxx.NullString(s.RecoveryNodeGroup()) - f.State = recovery.StateEmailSent + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewRecoveryEmailWithCodeSent()) f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) - f.UI.Nodes.Append(node.NewInputField("method", s.RecoveryNodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) f.UI. GetNodes(). diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go index 2adbf97213d4..10d5ae6f5135 100644 --- a/selfservice/strategy/code/strategy_recovery_test.go +++ b/selfservice/strategy/code/strategy_recovery_test.go @@ -412,7 +412,7 @@ func TestRecovery(t *testing.T) { assert.Len(t, gjson.Get(recoverySubmissionResponse, "ui.messages").Array(), 1, "%s", recoverySubmissionResponse) assertx.EqualAsJSON(t, text.NewRecoveryEmailWithCodeSent(), json.RawMessage(gjson.Get(recoverySubmissionResponse, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by entering the following code") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -697,7 +697,7 @@ func TestRecovery(t *testing.T) { assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==code).attributes.value").String(), "%s", body) assertx.EqualAsJSON(t, text.NewRecoveryEmailWithCodeSent(), json.RawMessage(gjson.Get(body, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Account access attempted") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Account access attempted") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } @@ -734,7 +734,7 @@ func TestRecovery(t *testing.T) { addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, email) assert.NoError(t, err) - emailText := testhelpers.CourierExpectMessage(t, reg, email, "Recover access to your account") + emailText := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, emailText, 1) // Deactivate the identity @@ -773,7 +773,7 @@ func TestRecovery(t *testing.T) { actual := expectSuccessfulRecovery(t, cl, RecoveryFlowTypeBrowser, func(v url.Values) { v.Set("email", email) }) - message := testhelpers.CourierExpectMessage(t, reg, email, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -834,7 +834,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }, http.StatusOK) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) form := withCSRFToken(t, testCase.FlowType, actual, url.Values{ @@ -945,7 +945,7 @@ func TestRecovery(t *testing.T) { initialFlowId := gjson.Get(body, "id") - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by entering the following code") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -1000,7 +1000,7 @@ func TestRecovery(t *testing.T) { assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, recoveryEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) submitRecoveryCode(t, c, body, RecoveryFlowTypeBrowser, recoveryCode, http.StatusOK) @@ -1019,14 +1019,14 @@ func TestRecovery(t *testing.T) { require.NotEmpty(t, action) assert.Equal(t, recoveryEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message1 := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message1 := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode1 := testhelpers.CourierExpectCodeInMessage(t, message1, 1) body = resendRecoveryCode(t, c, body, RecoveryFlowTypeBrowser, http.StatusOK) assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, recoveryEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message2 := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message2 := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode2 := testhelpers.CourierExpectCodeInMessage(t, message2, 1) body = submitRecoveryCode(t, c, body, RecoveryFlowTypeBrowser, recoveryCode1, http.StatusOK) @@ -1070,7 +1070,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) action := gjson.Get(body, "ui.action").String() @@ -1102,7 +1102,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) action := gjson.Get(body, "ui.action").String() diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go new file mode 100644 index 000000000000..f4c97641f055 --- /dev/null +++ b/selfservice/strategy/code/strategy_registration.go @@ -0,0 +1,284 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "encoding/json" + "net/http" + "strings" + + "github.com/ory/herodot" + "github.com/ory/x/otelx" + + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +var _ registration.Strategy = new(Strategy) + +// Update Registration Flow with Code Method +// +// swagger:model updateRegistrationFlowWithCodeMethod +type updateRegistrationFlowWithCodeMethod struct { + // The identity's traits + // + // required: true + Traits json.RawMessage `json:"traits" form:"traits"` + + // The OTP Code sent to the user + // + // required: false + Code string `json:"code" form:"code"` + + // The CSRF Token + CSRFToken string `json:"csrf_token" form:"csrf_token"` + + // Method to use + // + // This field must be set to `code` when using the code method. + // + // required: true + Method string `json:"method" form:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` + + // Resend restarts the flow with a new code + // + // required: false + Resend string `json:"resend" form:"resend"` +} + +func (p *updateRegistrationFlowWithCodeMethod) GetResend() string { + return p.Resend +} + +func (s *Strategy) RegisterRegistrationRoutes(*x.RouterPublic) {} + +func (s *Strategy) HandleRegistrationError(ctx context.Context, r *http.Request, f *registration.Flow, body *updateRegistrationFlowWithCodeMethod, err error) error { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } + + if f != nil { + if body != nil { + action := f.AppendTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(ctx), registration.RouteSubmitFlow)).String() + for _, n := range container.NewFromJSON(action, node.CodeGroup, body.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } + + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + } + + return err +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, rf *registration.Flow) error { + return s.PopulateMethod(r, rf) +} + +type options func(*identity.Identity) error + +func WithCredentials(via identity.CodeAddressType, usedAt sql.NullTime) options { + return func(i *identity.Identity) error { + return i.SetCredentialsWithConfig(identity.CredentialsTypeCodeAuth, identity.Credentials{Type: identity.CredentialsTypePassword, Identifiers: []string{}}, &identity.CredentialsCode{AddressType: via, UsedAt: usedAt}) + } +} + +func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flow, traits json.RawMessage, transientPayload json.RawMessage, i *identity.Identity, opts ...options) error { + f.TransientPayload = transientPayload + if len(traits) == 0 { + traits = json.RawMessage("{}") + } + + // we explicitly set the Code credentials type + i.Traits = identity.Traits(traits) + if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, &identity.CredentialsCode{UsedAt: sql.NullTime{}}); err != nil { + return err + } + + for _, opt := range opts { + if err := opt(i); err != nil { + return err + } + } + + // Validate the identity + if err := s.deps.IdentityValidator().Validate(ctx, i); err != nil { + return err + } + + return nil +} + +func (s *Strategy) getCredentialsFromTraits(ctx context.Context, f *registration.Flow, i *identity.Identity, traits, transientPayload json.RawMessage) (*identity.Credentials, error) { + if err := s.handleIdentityTraits(ctx, f, traits, transientPayload, i); err != nil { + return nil, errors.WithStack(err) + } + + cred, ok := i.GetCredentials(identity.CredentialsTypeCodeAuth) + if !ok { + return nil, errors.WithStack(schema.NewMissingIdentifierError()) + } else if len(cred.Identifiers) == 0 { + return nil, errors.WithStack(schema.NewMissingIdentifierError()) + } + return cred, nil +} + +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Register") + defer otelx.End(span, &err) + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { + return err + } + + var p updateRegistrationFlowWithCodeMethod + if err := registration.DecodeBody(&p, r, s.dx, s.deps.Config(), registrationSchema); err != nil { + return s.HandleRegistrationError(ctx, r, f, &p, err) + } + + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { + return s.HandleRegistrationError(ctx, r, f, &p, err) + } + + // By Default the flow should be in the 'choose method' state. + SetDefaultFlowState(f, p.Resend) + + switch f.GetState() { + case flow.StateChooseMethod: + return s.HandleRegistrationError(ctx, r, f, &p, s.registrationSendEmail(ctx, w, r, f, &p, i)) + case flow.StateEmailSent: + return s.HandleRegistrationError(ctx, r, f, &p, s.registrationVerifyCode(ctx, f, &p, i)) + case flow.StatePassedChallenge: + return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(schema.NewNoRegistrationStrategyResponsible())) + } + + return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unexpected flow state: %s", f.GetState()))) +} + +func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.registrationSendEmail") + defer otelx.End(span, &err) + + if len(p.Traits) == 0 { + return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) + } + + // Create the Registration code + + // Step 1: validate the identity's traits + cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + if err != nil { + return err + } + + // Step 2: Delete any previous registration codes for this flow ID + if err := s.deps.RegistrationCodePersister().DeleteRegistrationCodesOfFlow(ctx, f.ID); err != nil { + return errors.WithStack(err) + } + + // Step 3: Get the identity email and send the code + var addresses []Address + for _, identifier := range cred.Identifiers { + addresses = append(addresses, Address{To: identifier, Via: identity.AddressTypeEmail}) + } + // kratos only supports `email` identifiers at the moment with the code method + // this is validated in the identity validation step above + if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { + return errors.WithStack(err) + } + + // sets the flow state to code sent + f.SetState(flow.NextState(f.GetState())) + + // Step 4: Generate the UI for the `code` input form + // re-initialize the UI with a "clean" new state + // this should also provide a "resend" button and an option to change the email address + if err := s.NewCodeUINodes(r, f, p.Traits); err != nil { + return errors.WithStack(err) + } + + f.Active = identity.CredentialsTypeCodeAuth + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + if x.IsJSONRequest(r) { + s.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther) + } + + // we return an error to the flow handler so that it does not continue execution of the hooks. + // we are not done with the registration flow yet. The user needs to verify the code and then we need to persist the identity. + return errors.WithStack(flow.ErrCompletedByStrategy) + +} + +func (s *Strategy) registrationVerifyCode(ctx context.Context, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.registrationVerifyCode") + defer otelx.End(span, &err) + + if len(p.Code) == 0 { + return errors.WithStack(schema.NewRequiredError("#/code", "code")) + } + + if len(p.Traits) == 0 { + return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) + } + + // Step 1: Re-validate the identity's traits + // this is important since the client could have switched out the identity's traits + // this method also returns the credentials for a temporary identity + cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + if err != nil { + return err + } + + // Step 2: Check if the flow traits match the identity traits + for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes { + if !strings.EqualFold(f.GetUI().GetNodes().Find(n.ID()).Attributes.GetValue().(string), n.Attributes.GetValue().(string)) { + return errors.WithStack(schema.NewTraitsMismatch()) + } + } + + // Step 3: Attempt to use the code + registrationCode, err := s.deps.RegistrationCodePersister().UseRegistrationCode(ctx, f.ID, p.Code, cred.Identifiers...) + if err != nil { + if errors.Is(err, ErrCodeNotFound) { + return errors.WithStack(schema.NewRegistrationCodeInvalid()) + } + return errors.WithStack(err) + } + + // Step 4: The code was correct, populate the Identity credentials and traits + if err := s.handleIdentityTraits(ctx, f, p.Traits, p.TransientPayload, i, WithCredentials(registrationCode.AddressType, registrationCode.UsedAt)); err != nil { + return errors.WithStack(err) + } + + // since nothing has errored yet, we can assume that the code is correct + // and we can update the registration flow + f.SetState(flow.NextState(f.GetState())) + + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go new file mode 100644 index 000000000000..d787b2b3fa4e --- /dev/null +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -0,0 +1,526 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + _ "embed" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" +) + +type state struct { + flowID string + client *http.Client + email string + testServer *httptest.Server + resultIdentity *identity.Identity +} + +func TestRegistrationCodeStrategyDisabled(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypePassword.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), false) + + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + client := testhelpers.NewClientWithCookies(t) + resp, err := client.Get(public.URL + registration.RouteInitBrowserFlow) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Falsef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.value==code)").Exists(), "%s", body) + + // attempt to still submit the code form even though it doesn't exist + + payload := strings.NewReader(url.Values{ + "csrf_token": {gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String()}, + "method": {"code"}, + "traits.email": {testhelpers.RandomEmail()}, + }.Encode()) + req, err := http.NewRequestWithContext(ctx, "POST", public.URL+registration.RouteSubmitFlow+"?flow="+gjson.GetBytes(body, "id").String(), payload) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err = client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "This endpoint was disabled by system administrator. Please check your url or contact the system administrator to enable it.", gjson.GetBytes(body, "error.reason").String()) +} + +func TestRegistrationCodeStrategy(t *testing.T) { + setup := func(ctx context.Context, t *testing.T) (*config.Config, *driver.RegistryDefault, *httptest.Server) { + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypePassword.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), true) + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) + + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + return conf, reg, public + } + + createRegistrationFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, isSPA bool) *state { + t.Helper() + + client := testhelpers.NewClientWithCookies(t) + client.Transport = testhelpers.NewTransportWithLogger(http.DefaultTransport, t).RoundTripper + clientInit := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, public, isSPA, false, false) + + body, err := json.Marshal(clientInit) + require.NoError(t, err) + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmpty(t, csrfToken) + + require.Truef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email)").Exists(), "%s", body) + require.Truef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.value==code)").Exists(), "%s", body) + + return &state{ + client: client, + flowID: clientInit.GetId(), + testServer: public, + } + } + + type onSubmitAssertion func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) + + registerNewUser := func(ctx context.Context, t *testing.T, s *state, isSPA bool, submitAssertion onSubmitAssertion) *state { + t.Helper() + + if s.email == "" { + s.email = testhelpers.RandomEmail() + } + + rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + values := testhelpers.SDKFormFieldsToURLValues(rf.Ui.Nodes) + values.Set("traits.email", s.email) + values.Set("method", "code") + + body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) + + if submitAssertion != nil { + submitAssertion(ctx, t, s, body, resp) + return s + } + + if isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + assert.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + + return s + } + + submitOTP := func(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, s *state, vals func(v *url.Values), isSPA bool, submitAssertion onSubmitAssertion) *state { + t.Helper() + + rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + values := testhelpers.SDKFormFieldsToURLValues(rf.Ui.Nodes) + // the sdk to values always adds resend which isn't what we always need here. + // so we delete it here. + // the custom vals func can add it again if needed. + values.Del("resend") + values.Set("traits.email", s.email) + vals(&values) + + body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) + + if submitAssertion != nil { + submitAssertion(ctx, t, s, body, resp) + return s + } + + require.Equal(t, http.StatusOK, resp.StatusCode) + + verifiableAddress, err := reg.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, s.email) + require.NoError(t, err) + require.Equal(t, strings.ToLower(s.email), verifiableAddress.Value) + + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, verifiableAddress.IdentityID) + require.NoError(t, err) + require.NotNil(t, id.ID) + + _, ok := id.GetCredentials(identity.CredentialsTypeCodeAuth) + require.True(t, ok) + + s.resultIdentity = id + return s + } + + t.Run("test=different flows on the same configurations", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + _, reg, public := setup(ctx, t) + + for _, tc := range []struct { + d string + isSPA bool + }{ + { + d: "SPA client", + isSPA: true, + }, + { + d: "Browser client", + isSPA: false, + }, + } { + t.Run("flow="+tc.d, func(t *testing.T) { + t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, nil) + }) + + t.Run("case=should normalize email address on sign up", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.isSPA) + sourceMail := testhelpers.RandomEmail() + state.email = strings.ToUpper(sourceMail) + assert.NotEqual(t, sourceMail, state.email) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, sourceMail, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, nil) + + creds, ok := state.resultIdentity.GetCredentials(identity.CredentialsTypeCodeAuth) + require.True(t, ok) + require.Len(t, creds.Identifiers, 1) + assert.Equal(t, sourceMail, creds.Identifiers[0]) + }) + + t.Run("case=should be able to resend the code", func(t *testing.T) { + ctx := context.Background() + + s := createRegistrationFlow(ctx, t, public, tc.isSPA) + + s = registerNewUser(ctx, t, s, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + + attr := gjson.Get(body, "ui.nodes.#(attributes.name==method)#").String() + require.NotEmpty(t, attr) + + val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() + require.Equal(t, "code", val) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // resend code + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("resend", "code") + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode) + } + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmptyf(t, csrfToken, "%s", body) + require.Containsf(t, gjson.Get(body, "ui.messages").String(), "An email containing a code has been sent to the email address you provided.", "%s", body) + }) + + // get the new code from email + message = testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode2) + + require.NotEqual(t, registrationCode, registrationCode2) + + // try submit old code + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") + }) + + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode2) + }, tc.isSPA, nil) + }) + + t.Run("case=swapping out traits should not be possible on code submit", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s.email = "not-" + s.email // swap out email + + // 3. Submit OTP + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + }) + }) + + t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, nil) + + reg.Persister().Transaction(ctx, func(ctx context.Context, connection *pop.Connection) error { + count, err := connection.RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ?", new(code.RegistrationCode).TableName(ctx)), uuid.FromStringOrNil(s.flowID)).Count(new(code.RegistrationCode)) + require.NoError(t, err) + require.Equal(t, 1, count) + return nil + }) + + for i := 0; i < 5; i++ { + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", "111111") + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + }) + } + + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", "111111") + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) + }) + } + }) + + t.Run("test=cases with different configs", func(t *testing.T) { + ctx := context.Background() + conf, reg, public := setup(ctx, t) + + for _, tc := range []struct { + d string + isSPA bool + }{ + { + d: "SPA client", + isSPA: true, + }, + { + d: "Browser client", + isSPA: false, + }, + } { + t.Run("test="+tc.d, func(t *testing.T) { + t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages").String(), "Could not find any login identifiers") + } else { + // we expect a redirect to the registration page with the flow id + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, conf.SelfServiceFlowRegistrationUI(ctx).Path, resp.Request.URL.Path) + rf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetRegistrationFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(rf) + require.NoError(t, err) + require.Contains(t, gjson.GetBytes(body, "ui.messages").String(), "Could not find any login identifiers") + + } + }) + }) + + t.Run("case=should have verifiable address even if after session hook is disabled", func(t *testing.T) { + // disable the after session hook + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{}) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) + }) + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, nil) + }) + + t.Run("case=code should expire", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "10ns") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1h") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusGone, resp.StatusCode) + require.Containsf(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago", "%s", body) + } else { + // with browser clients we redirect back to the UI with a new flow id as a query parameter + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, conf.SelfServiceFlowRegistrationUI(ctx).Path, resp.Request.URL.Path) + require.NotEqual(t, s.flowID, resp.Request.URL.Query().Get("flow")) + } + }) + }) + }) + } + }) +} diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index 9a0902a404bb..c02e89105116 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -38,35 +38,7 @@ func (s *Strategy) RegisterAdminVerificationRoutes(admin *x.RouterAdmin) { // Otherwise, the default email input is added. // If the flow is a browser flow, the CSRF token is added to the UI. func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error { - nodes := node.Nodes{} - switch f.State { - case verification.StateEmailSent: - nodes.Upsert( - node. - NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). - WithMetaLabel(text.NewInfoNodeLabelVerificationCode()), - ) - // Required for the re-send code button - nodes.Append( - node.NewInputField("method", s.VerificationNodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), - ) - f.UI.Messages.Set(text.NewVerificationEmailWithCodeSent()) - default: - nodes.Upsert( - node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). - WithMetaLabel(text.NewInfoNodeInputEmail()), - ) - } - nodes.Append( - node.NewInputField("method", s.VerificationStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit()), - ) - - f.UI.Nodes = nodes - if f.Type == flow.TypeBrowser { - f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - } - return nil + return s.PopulateMethod(r, f) } func (s *Strategy) decodeVerification(r *http.Request) (*updateVerificationFlowWithCodeMethod, error) { @@ -156,7 +128,7 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio return s.handleVerificationError(w, r, nil, body, err) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil { return s.handleVerificationError(w, r, f, body, err) } @@ -165,11 +137,11 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } switch f.State { - case verification.StateChooseMethod: + case flow.StateChooseMethod: fallthrough - case verification.StateEmailSent: + case flow.StateEmailSent: return s.verificationHandleFormSubmission(w, r, f, body) - case verification.StatePassedChallenge: + case flow.StatePassedChallenge: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) default: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) @@ -177,7 +149,6 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } func (s *Strategy) handleLinkClick(w http.ResponseWriter, r *http.Request, f *verification.Flow, code string) error { - // Pre-fill the code if codeField := f.UI.Nodes.Find("code"); codeField != nil { codeField.Attributes.SetValue(code) @@ -230,7 +201,7 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht // Continue execution } - f.State = verification.StateEmailSent + f.State = flow.StateEmailSent if err := s.PopulateVerificationMethod(r, f); err != nil { return s.handleVerificationError(w, r, f, body, err) @@ -265,10 +236,6 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, f.Type, err) } - if err := code.Validate(); err != nil { - return s.retryVerificationFlowWithError(w, r, f.Type, err) - } - i, err := s.deps.IdentityPool().GetIdentity(r.Context(), code.VerifiableAddress.IdentityID, identity.ExpandDefault) if err != nil { return s.retryVerificationFlowWithError(w, r, f.Type, err) @@ -294,7 +261,7 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c Action: returnTo.String(), } - f.State = verification.StatePassedChallenge + f.State = flow.StatePassedChallenge // See https://github.com/ory/kratos/issues/1547 f.SetCSRFToken(flow.GetCSRFToken(s.deps, w, r, f.Type)) f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) @@ -378,7 +345,6 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http } func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress) (err error) { - rawCode := GenerateCode() code, err := s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{ @@ -387,7 +353,6 @@ func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Fl VerifiableAddress: a, FlowID: f.ID, }) - if err != nil { return err } diff --git a/selfservice/strategy/code/strategy_verification_test.go b/selfservice/strategy/code/strategy_verification_test.go index b00bcda5c2bf..9be8cd08145d 100644 --- a/selfservice/strategy/code/strategy_verification_test.go +++ b/selfservice/strategy/code/strategy_verification_test.go @@ -43,7 +43,7 @@ func TestVerification(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) initViper(t, ctx, conf) - var identityToVerify = &identity.Identity{ + identityToVerify := &identity.Identity{ ID: x.NewUUID(), Traits: identity.Traits(`{"email":"verifyme@ory.sh"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, @@ -56,7 +56,7 @@ func TestVerification(t *testing.T) { }, } - var verificationEmail = gjson.GetBytes(identityToVerify.Traits, "email").String() + verificationEmail := gjson.GetBytes(identityToVerify.Traits, "email").String() _ = testhelpers.NewVerificationUIFlowEchoServer(t, reg) _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) @@ -69,7 +69,7 @@ func TestVerification(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToVerify, identity.ManagerAllowWriteProtectedTraits)) - var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { if hc == nil { hc = testhelpers.NewDebugClient(t) if !isAPI { @@ -82,15 +82,15 @@ func TestVerification(t *testing.T) { testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) } - var expectValidationError = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) } - var expectSuccess = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, http.StatusOK) } - var submitVerificationCode = func(t *testing.T, body string, c *http.Client, code string) (string, *http.Response) { + submitVerificationCode := func(t *testing.T, body string, c *http.Client, code string) (string, *http.Response) { action := gjson.Get(body, "ui.action").String() require.NotEmpty(t, action, "%v", string(body)) csrfToken := extractCsrfToken([]byte(body)) @@ -135,14 +135,14 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require an email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, "Property email is missing.", gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Del("email") } @@ -160,7 +160,7 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require a valid email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string, value string) { + check := func(t *testing.T, actual string, value string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value), gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), @@ -168,7 +168,7 @@ func TestVerification(t *testing.T) { } for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -194,16 +194,16 @@ func TestVerification(t *testing.T) { }) var email string - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Someone tried to verify this email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Someone tried to verify this email address") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -282,7 +282,7 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by entering the following code") code := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -295,12 +295,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by entering the following code") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -335,7 +335,7 @@ func TestVerification(t *testing.T) { assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now())) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } @@ -353,13 +353,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address when the link is opened in another browser", func(t *testing.T) { - - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } expectSuccess(t, nil, false, false, values) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) code := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -377,7 +376,7 @@ func TestVerification(t *testing.T) { newValidFlow := func(t *testing.T, fType flow.Type, requestURL string) (*verification.Flow, *code.VerificationCode, string) { f, err := verification.NewFlow(conf, time.Hour, x.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), code.NewStrategy(reg), fType) require.NoError(t, err) - f.State = verification.StateEmailSent + f.State = flow.StateEmailSent require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID) identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email) @@ -422,7 +421,7 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by entering the following code") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -459,7 +458,7 @@ func TestVerification(t *testing.T) { assert.Equal(t, text.ErrIDSelfServiceFlowReplaced, gjson.GetBytes(f2, "error.id").String()) }) - var resendVerificationCode = func(t *testing.T, client *http.Client, flow string, flowType string, statusCode int) string { + resendVerificationCode := func(t *testing.T, client *http.Client, flow string, flowType string, statusCode int) string { action := gjson.Get(flow, "ui.action").String() assert.NotEmpty(t, action) @@ -487,7 +486,7 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") _ = testhelpers.CourierExpectCodeInMessage(t, message, 1) c := testhelpers.NewClientWithCookies(t) @@ -496,19 +495,18 @@ func TestVerification(t *testing.T) { assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, verificationEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message = testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") verificationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) submitVerificationCode(t, body, c, verificationCode) }) t.Run("case=should not be able to use first code after resending code", func(t *testing.T) { - body := expectSuccess(t, nil, true, false, func(v url.Values) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") firstCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) c := testhelpers.NewClientWithCookies(t) @@ -517,7 +515,7 @@ func TestVerification(t *testing.T) { assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, verificationEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message = testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") secondCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) body, res := submitVerificationCode(t, body, c, firstCode) @@ -568,7 +566,7 @@ func TestVerification(t *testing.T) { body := expectSuccess(t, nil, true, false, func(v url.Values) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") code := testhelpers.CourierExpectCodeInMessage(t, message, 1) body, res := submitVerificationCode(t, body, c, code) @@ -578,7 +576,7 @@ func TestVerification(t *testing.T) { body = expectSuccess(t, nil, true, false, func(v url.Values) { v.Set("email", verificationEmail) }) - message = testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") code = testhelpers.CourierExpectCodeInMessage(t, message, 1) body, res = submitVerificationCode(t, body, c, code) @@ -636,5 +634,4 @@ func TestVerification(t *testing.T) { }) } }) - } diff --git a/selfservice/strategy/code/stub/code.identity.schema.json b/selfservice/strategy/code/stub/code.identity.schema.json new file mode 100644 index 000000000000..f8d988c21af9 --- /dev/null +++ b/selfservice/strategy/code/stub/code.identity.schema.json @@ -0,0 +1,61 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "email_0": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "email_1": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index cf8e25afc155..f505bcb9c185 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -10,6 +10,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/x/randx" @@ -25,7 +26,8 @@ import ( func TestPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { +}, +) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) @@ -33,10 +35,11 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("code=recovery", func(t *testing.T) { - newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) { var f recovery.Flow require.NoError(t, faker.FakeData(&f)) + f.State = flow.StateChooseMethod + require.NoError(t, p.CreateRecoveryFlow(ctx, &f)) var i identity.Identity @@ -141,7 +144,6 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { count, err = p.GetConnection(ctx).Where("selfservice_recovery_flow_id = ?", f.ID).Count(&code.RecoveryCode{}) require.NoError(t, err) require.Equal(t, 0, count) - }) }) } diff --git a/selfservice/strategy/link/strategy.go b/selfservice/strategy/link/strategy.go index fa5e9218a1df..da66e1816bf5 100644 --- a/selfservice/strategy/link/strategy.go +++ b/selfservice/strategy/link/strategy.go @@ -19,13 +19,17 @@ import ( "github.com/ory/x/decoderx" ) -var _ recovery.Strategy = new(Strategy) -var _ recovery.AdminHandler = new(Strategy) -var _ recovery.PublicHandler = new(Strategy) +var ( + _ recovery.Strategy = new(Strategy) + _ recovery.AdminHandler = new(Strategy) + _ recovery.PublicHandler = new(Strategy) +) -var _ verification.Strategy = new(Strategy) -var _ verification.AdminHandler = new(Strategy) -var _ verification.PublicHandler = new(Strategy) +var ( + _ verification.Strategy = new(Strategy) + _ verification.AdminHandler = new(Strategy) + _ verification.PublicHandler = new(Strategy) +) type ( // FlowMethod contains the configuration for this selfservice strategy. @@ -83,10 +87,6 @@ func NewStrategy(d strategyDependencies) *Strategy { return &Strategy{d: d, dx: decoderx.NewHTTP()} } -func (s *Strategy) RecoveryNodeGroup() node.UiNodeGroup { - return node.LinkGroup -} - -func (s *Strategy) VerificationNodeGroup() node.UiNodeGroup { +func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.LinkGroup } diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 6ab3ff4ae904..da4f40ac9516 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -40,7 +40,6 @@ func (s *Strategy) RecoveryStrategyID() string { func (s *Strategy) RegisterPublicRecoveryRoutes(public *x.RouterPublic) { s.d.CSRFHandler().IgnorePath(RouteAdminCreateRecoveryLink) public.POST(RouteAdminCreateRecoveryLink, x.RedirectToAdminRoute(s.d)) - } func (s *Strategy) RegisterAdminRecoveryRoutes(admin *x.RouterAdmin) { @@ -198,7 +197,8 @@ func (s *Strategy) createRecoveryLinkForIdentity(w http.ResponseWriter, r *http. url.Values{ "token": {token.Token}, "flow": {req.ID.String()}, - }).String()}, + }).String(), + }, herodot.UnescapedHTML) } @@ -237,7 +237,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } if len(body.Token) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } @@ -253,7 +253,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return errors.WithStack(flow.ErrCompletedByStrategy) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.RecoveryStrategyID(), body.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), body.Method, s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } @@ -267,11 +267,11 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch req.State { - case recovery.StateChooseMethod: + case flow.StateChooseMethod: fallthrough - case recovery.StateEmailSent: + case flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, req) - case recovery.StatePassedChallenge: + case flow.StatePassedChallenge: // was already handled, do not allow retry return s.retryRecoveryFlowWithMessage(w, r, req.Type, text.NewErrorValidationRecoveryRetrySuccess()) default: @@ -281,7 +281,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, f *recovery.Flow, id *identity.Identity) error { f.UI.Messages.Clear() - f.State = recovery.StatePassedChallenge + f.State = flow.StatePassedChallenge f.SetCSRFToken(s.d.CSRFHandler().RegenerateToken(w, r)) f.RecoveredIdentityID = uuid.NullUUID{ UUID: id.ID, @@ -455,8 +455,8 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.Active = sqlxx.NullString(s.RecoveryNodeGroup()) - f.State = recovery.StateEmailSent + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewRecoveryEmailSent()) if err := s.d.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { return s.HandleRecoveryError(w, r, f, body, err) diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 4af007577500..71518cda33be 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -61,9 +61,10 @@ func init() { } func createIdentityToRecover(t *testing.T, reg *driver.RegistryDefault, email string) *identity.Identity { - var id = &identity.Identity{ + id := &identity.Identity{ Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}}, + "password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), SchemaID: config.DefaultIdentityTraitsSchemaID, } @@ -273,7 +274,7 @@ func TestRecovery(t *testing.T) { public, _, publicRouter, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) - var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { if hc == nil { hc = testhelpers.NewDebugClient(t) if !isAPI { @@ -286,11 +287,11 @@ func TestRecovery(t *testing.T) { testhelpers.ExpectURL(isAPI || isSPA, public.URL+recovery.RouteSubmitFlow, conf.SelfServiceFlowRecoveryUI(ctx).String())) } - var expectValidationError = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) } - var expectSuccess = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, http.StatusOK) } @@ -311,14 +312,14 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should require an email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, "Property email is missing.", gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Del("email") } @@ -336,14 +337,14 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should require a valid email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string, value string) { + check := func(t *testing.T, actual string, value string) { assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value), gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -422,16 +423,16 @@ func TestRecovery(t *testing.T) { conf.Set(ctx, config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients, false) }) var email string - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Account access attempted") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Account access attempted") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -452,11 +453,11 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should not be able to recover an inactive account", func(t *testing.T) { - var check = func(t *testing.T, recoverySubmissionResponse, recoveryEmail string, isAPI bool) { + check := func(t *testing.T, recoverySubmissionResponse, recoveryEmail string, isAPI bool) { addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) assert.NoError(t, err) - recoveryLink := testhelpers.CourierExpectLinkInMessage(t, testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account"), 1) + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account"), 1) cl := testhelpers.NewClientWithCookies(t) // Deactivate the identity @@ -503,7 +504,7 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should recover an account", func(t *testing.T) { - var check = func(t *testing.T, recoverySubmissionResponse, recoveryEmail, returnTo string) { + check := func(t *testing.T, recoverySubmissionResponse, recoveryEmail, returnTo string) { addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) assert.NoError(t, err) assert.False(t, addr.Verified) @@ -515,7 +516,7 @@ func TestRecovery(t *testing.T) { require.Len(t, gjson.Get(recoverySubmissionResponse, "ui.messages").Array(), 1, "%s", recoverySubmissionResponse) assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(recoverySubmissionResponse, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by clicking the following link") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -634,8 +635,8 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should recover an account and set the csrf cookies", func(t *testing.T) { - var check = func(t *testing.T, actual, recoveryEmail string, cl *http.Client, do func(*http.Client, *http.Request) (*http.Response, error)) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual, recoveryEmail string, cl *http.Client, do func(*http.Client, *http.Request) (*http.Response, error)) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -659,21 +660,21 @@ func TestRecovery(t *testing.T) { body := x.MustReadAll(actualRes.Body) require.NoError(t, actualRes.Body.Close()) assert.Equal(t, http.StatusOK, actualRes.StatusCode, "%s", body) - assert.Equal(t, string(recovery.StatePassedChallenge), gjson.GetBytes(body, "state").String(), "%s", body) + assert.Equal(t, string(flow.StatePassedChallenge), gjson.GetBytes(body, "state").String(), "%s", body) } email := x.NewUUID().String() + "@ory.sh" id := createIdentityToRecover(t, reg, email) t.Run("case=unauthenticated", func(t *testing.T) { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } check(t, expectSuccess(t, nil, false, false, values), email, testhelpers.NewClientWithCookies(t), (*http.Client).Do) }) t.Run("case=already logged into another account", func(t *testing.T) { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -684,7 +685,7 @@ func TestRecovery(t *testing.T) { }) t.Run("case=already logged into the account", func(t *testing.T) { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -715,8 +716,8 @@ func TestRecovery(t *testing.T) { require.NoError(t, err) assert.True(t, actualSession.IsActive()) - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -736,7 +737,7 @@ func TestRecovery(t *testing.T) { assert.False(t, actualSession.IsActive()) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", recoveryEmail) } @@ -797,7 +798,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by clicking the following link") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -833,8 +834,8 @@ func TestRecovery(t *testing.T) { recoveryEmail := testhelpers.RandomEmail() createIdentityToRecover(t, reg, recoveryEmail) - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -850,7 +851,7 @@ func TestRecovery(t *testing.T) { assert.Contains(t, cookies, "ory_kratos_session") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", recoveryEmail) } @@ -866,8 +867,8 @@ func TestRecovery(t *testing.T) { recoveryEmail := testhelpers.RandomEmail() createIdentityToRecover(t, reg, recoveryEmail) - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -883,7 +884,7 @@ func TestRecovery(t *testing.T) { assert.NotContains(t, cookies, "ory_kratos_session") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", recoveryEmail) } diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index e09ffb39f603..47271cd7191d 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -122,14 +122,14 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } if len(body.Token) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { return s.handleVerificationError(w, r, nil, body, err) } return s.verificationUseToken(w, r, body, f) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.VerificationStrategyID(), body.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), body.Method, s.d); err != nil { return s.handleVerificationError(w, r, f, body, err) } @@ -138,12 +138,12 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } switch f.State { - case verification.StateChooseMethod: + case flow.StateChooseMethod: fallthrough - case verification.StateEmailSent: + case flow.StateEmailSent: // Do nothing (continue with execution after this switch statement) return s.verificationHandleFormSubmission(w, r, f) - case verification.StatePassedChallenge: + case flow.StatePassedChallenge: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) default: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) @@ -151,7 +151,6 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *http.Request, f *verification.Flow) error { - var body = new(verificationSubmitPayload) body, err := s.decodeVerification(r) if err != nil { return s.handleVerificationError(w, r, f, body, err) @@ -178,8 +177,8 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.Active = sqlxx.NullString(s.VerificationNodeGroup()) - f.State = verification.StateEmailSent + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewVerificationEmailSent()) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { return s.handleVerificationError(w, r, f, body, err) @@ -232,7 +231,7 @@ func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, Action: returnTo.String(), } f.UI.Messages.Clear() - f.State = verification.StatePassedChallenge + f.State = flow.StatePassedChallenge // See https://github.com/ory/kratos/issues/1547 f.SetCSRFToken(flow.GetCSRFToken(s.d, w, r, f.Type)) f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) @@ -304,7 +303,6 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http } func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress) error { - token := NewSelfServiceVerificationToken(a, f, s.d.Config().SelfServiceLinkMethodLifespan(ctx)) if err := s.d.VerificationTokenPersister().CreateVerificationToken(ctx, token); err != nil { return err diff --git a/selfservice/strategy/link/strategy_verification_test.go b/selfservice/strategy/link/strategy_verification_test.go index 474107292f2f..c81834e32b91 100644 --- a/selfservice/strategy/link/strategy_verification_test.go +++ b/selfservice/strategy/link/strategy_verification_test.go @@ -41,15 +41,16 @@ func TestVerification(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) initViper(t, conf) - var identityToVerify = &identity.Identity{ + identityToVerify := &identity.Identity{ ID: x.NewUUID(), Traits: identity.Traits(`{"email":"verifyme@ory.sh"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", Identifiers: []string{"recoverme@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}}, + "password": {Type: "password", Identifiers: []string{"recoverme@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, } - var verificationEmail = gjson.GetBytes(identityToVerify.Traits, "email").String() + verificationEmail := gjson.GetBytes(identityToVerify.Traits, "email").String() _ = testhelpers.NewVerificationUIFlowEchoServer(t, reg) _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) @@ -62,7 +63,7 @@ func TestVerification(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToVerify, identity.ManagerAllowWriteProtectedTraits)) - var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { if hc == nil { hc = testhelpers.NewDebugClient(t) if !isAPI { @@ -75,11 +76,11 @@ func TestVerification(t *testing.T) { testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) } - var expectValidationError = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) } - var expectSuccess = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, http.StatusOK) } @@ -114,14 +115,14 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require an email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, "Property email is missing.", gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Del("email") } @@ -139,7 +140,7 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require a valid email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string, value string) { + check := func(t *testing.T, actual string, value string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value), gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), @@ -147,7 +148,7 @@ func TestVerification(t *testing.T) { } for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -172,16 +173,16 @@ func TestVerification(t *testing.T) { conf.Set(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, false) }) var email string - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Someone tried to verify this email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Someone tried to verify this email address") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -245,14 +246,14 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "Hi, please verify your account by clicking the following link") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) time.Sleep(time.Millisecond * 201) - //Clear cookies as link might be opened in another browser + // Clear cookies as link might be opened in another browser c = testhelpers.NewClientWithCookies(t) res, err := c.Get(verificationLink) require.NoError(t, err) @@ -269,12 +270,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by clicking the following link") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -304,7 +305,7 @@ func TestVerification(t *testing.T) { assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now())) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } @@ -322,8 +323,8 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address when the link is opened in another browser", func(t *testing.T) { - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -344,7 +345,7 @@ func TestVerification(t *testing.T) { assert.EqualValues(t, "passed_challenge", gjson.Get(actualBody, "state").String()) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } @@ -354,7 +355,7 @@ func TestVerification(t *testing.T) { newValidFlow := func(t *testing.T, fType flow.Type, requestURL string) (*verification.Flow, *link.VerificationToken) { f, err := verification.NewFlow(conf, time.Hour, x.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), nil, fType) require.NoError(t, err) - f.State = verification.StateEmailSent + f.State = flow.StateEmailSent require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID) identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email) @@ -412,7 +413,6 @@ func TestVerification(t *testing.T) { }) t.Run("case=should not be able to use code from different flow", func(t *testing.T) { - f1, _ := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow) _, t2 := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow) diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index ae669a888453..63abe6179f18 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -10,6 +10,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/x/sqlcon" @@ -29,7 +30,8 @@ import ( func TestPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { +}, +) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) @@ -37,10 +39,10 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("token=recovery", func(t *testing.T) { - newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) { var req recovery.Flow require.NoError(t, faker.FakeData(&req)) + req.State = flow.StateChooseMethod require.NoError(t, p.CreateRecoveryFlow(ctx, &req)) var i identity.Identity @@ -122,13 +124,13 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { require.Error(t, err) }) }) - }) t.Run("token=verification", func(t *testing.T) { newVerificationToken := func(t *testing.T, email string) (*verification.Flow, *link.VerificationToken) { var f verification.Flow require.NoError(t, faker.FakeData(&f)) + f.State = flow.StateChooseMethod require.NoError(t, p.CreateVerificationFlow(ctx, &f)) var i identity.Identity diff --git a/selfservice/strategy/lookup/login.go b/selfservice/strategy/lookup/login.go index b5e5fddda7e8..75edc2181059 100644 --- a/selfservice/strategy/lookup/login.go +++ b/selfservice/strategy/lookup/login.go @@ -94,7 +94,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return nil, err } diff --git a/selfservice/strategy/lookup/settings.go b/selfservice/strategy/lookup/settings.go index 261336ecdcbc..6f61966e353c 100644 --- a/selfservice/strategy/lookup/settings.go +++ b/selfservice/strategy/lookup/settings.go @@ -108,7 +108,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. if p.RegenerateLookup || p.RevealLookup || p.ConfirmLookup || p.DisableLookup { // This method has only two submit buttons p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) } } else { @@ -141,7 +141,7 @@ func (s *Strategy) continueSettingsFlow( ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod, ) error { if p.ConfirmLookup || p.RevealLookup || p.RegenerateLookup || p.DisableLookup { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return err } diff --git a/selfservice/strategy/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go index 92a81e964971..fce2be4c0974 100644 --- a/selfservice/strategy/lookup/settings_test.go +++ b/selfservice/strategy/lookup/settings_test.go @@ -273,7 +273,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=can not confirm without regenerate", func(t *testing.T) { id, codes := createIdentity(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.LookupConfirm, "true") } @@ -310,7 +310,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=regenerate but no confirmation", func(t *testing.T) { id, codes := createIdentity(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.LookupRegenerate, "true") } @@ -363,13 +363,13 @@ func TestCompleteSettings(t *testing.T) { }, } { t.Run("credentials="+tc.d, func(t *testing.T) { - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Del(node.LookupReveal) v.Del(node.LookupDisable) v.Set(node.LookupRegenerate, "true") } - var payloadConfirm = func(v url.Values) { + payloadConfirm := func(v url.Values) { v.Del(node.LookupRegenerate) v.Del(node.LookupDisable) v.Del(node.LookupReveal) @@ -401,7 +401,7 @@ func TestCompleteSettings(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, apiClient, publicTS, "aal2", string(identity.CredentialsTypeLookup)) @@ -427,7 +427,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, browserClient, publicTS, "aal2", string(identity.CredentialsTypeLookup)) } @@ -463,7 +463,7 @@ func TestCompleteSettings(t *testing.T) { }, } { t.Run("credentials="+tc.d, func(t *testing.T) { - var payloadConfirm = func(v url.Values) { + payloadConfirm := func(v url.Values) { v.Del(node.LookupRegenerate) v.Del(node.LookupReveal) v.Set(node.LookupDisable, "true") @@ -489,7 +489,7 @@ func TestCompleteSettings(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, apiClient, publicTS, "aal1") @@ -512,7 +512,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, browserClient, publicTS, "aal1") } diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 2639868275c1..1b4f9ca56034 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -146,12 +146,15 @@ func generateState(flowID string) *State { Data: x.NewUUID().Bytes(), } } + func (s *State) setCode(code string) { s.Data = sha512.New().Sum([]byte(code)) } + func (s *State) codeMatches(code string) bool { return bytes.Equal(s.Data, sha512.New().Sum([]byte(code))) } + func parseState(s string) (*State, error) { raw, err := base64.RawURLEncoding.DecodeString(s) if err != nil { diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 23f4ff60514a..24af92562f5c 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -167,12 +167,12 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleError(w, r, f, "", nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP form request: %s", err.Error()))) } - var pid = p.Provider // this can come from both url query and post body + pid := p.Provider // this can come from both url query and post body if pid == "" { return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return nil, s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 464f4ad5796f..debc45d770db 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -137,12 +137,12 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat f.TransientPayload = p.TransientPayload - var pid = p.Provider // this can come from both url query and post body + pid := p.Provider // this can come from both url query and post body if pid == "" { return errors.WithStack(flow.ErrStrategyNotResponsible) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index d7497e567ab3..69f2bc03a560 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -76,41 +76,59 @@ func TestSettingsStrategy(t *testing.T) { // Make test data for this test run unique testID := x.NewUUID().String() users := map[string]*identity.Identity{ - "password": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"john` + testID + `@doe.com"}`), + "password": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"john` + testID + `@doe.com"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", + "password": { + Type: "password", Identifiers: []string{"john+" + testID + "@doe.com"}, - Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`)}}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + }, }, - "oryer": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+` + testID + `@ory.sh"}`), + "oryer": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+` + testID + `@ory.sh"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+` + testID + `"}]}`), + }, + }, }, - "githuber": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+github+` + testID + `@ory.sh"}`), + "githuber": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+github+` + testID + `@ory.sh"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+github+" + testID, "github:hackerman+github+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+github+` + testID + `"},{"provider":"github","subject":"hackerman+github+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+github+` + testID + `"},{"provider":"github","subject":"hackerman+github+` + testID + `"}]}`), + }, + }, SchemaID: config.DefaultIdentityTraitsSchemaID, }, - "multiuser": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+multiuser+` + testID + `@ory.sh"}`), + "multiuser": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+multiuser+` + testID + `@ory.sh"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", + "password": { + Type: "password", Identifiers: []string{"hackerman+multiuser+" + testID + "@ory.sh"}, - Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`)}, - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+multiuser+" + testID, "google:hackerman+multiuser+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+multiuser+` + testID + `"},{"provider":"google","subject":"hackerman+multiuser+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+multiuser+` + testID + `"},{"provider":"google","subject":"hackerman+multiuser+` + testID + `"}]}`), + }, + }, SchemaID: config.DefaultIdentityTraitsSchemaID, }, } agents := testhelpers.AddAndLoginIdentities(t, reg, publicTS, users) - var newProfileFlow = func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *settings.Flow { + newProfileFlow := func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *settings.Flow { req, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), x.ParseUUID(string(testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS).Id))) require.NoError(t, err) @@ -131,7 +149,7 @@ func TestSettingsStrategy(t *testing.T) { } // does the same as new profile request but uses the SDK - var nprSDK = func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *kratos.SettingsFlow { + nprSDK := func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *kratos.SettingsFlow { return testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS) } @@ -208,11 +226,11 @@ func TestSettingsStrategy(t *testing.T) { } }) - var action = func(req *kratos.SettingsFlow) string { + action := func(req *kratos.SettingsFlow) string { return req.Ui.Action } - var checkCredentials = func(t *testing.T, shouldExist bool, iid uuid.UUID, provider, subject string, expectTokens bool) { + checkCredentials := func(t *testing.T, shouldExist bool, iid uuid.UUID, provider, subject string, expectTokens bool) { actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), iid) require.NoError(t, err) @@ -242,7 +260,7 @@ func TestSettingsStrategy(t *testing.T) { require.EqualValues(t, shouldExist, found) } - var reset = func(t *testing.T) func() { + reset := func(t *testing.T) func() { return func() { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Minute*5) agents = testhelpers.AddAndLoginIdentities(t, reg, publicTS, users) @@ -250,20 +268,20 @@ func TestSettingsStrategy(t *testing.T) { } t.Run("suite=unlink", func(t *testing.T) { - var unlink = func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + unlink := func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { req = nprSDK(t, agents[agent], "", time.Hour) body, res = testhelpers.HTTPPostForm(t, agents[agent], action(req), &url.Values{"csrf_token": {x.FakeCSRFToken}, "unlink": {provider}}) return } - var unlinkInvalid = func(agent, provider, errorMessage string) func(t *testing.T) { + unlinkInvalid := func(agent, provider, errorMessage string) func(t *testing.T) { return func(t *testing.T) { body, res, req := unlink(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) - //assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) + // assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) // The original options to link google and github are still there t.Run("flow=fetch", func(t *testing.T) { @@ -302,7 +320,7 @@ func TestSettingsStrategy(t *testing.T) { t.Run("case=should not be able to unlink a connection without a privileged session", func(t *testing.T) { agent, provider := "githuber", "github" - var runUnauthed = func(t *testing.T) *kratos.SettingsFlow { + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) time.Sleep(time.Millisecond) t.Cleanup(reset(t)) @@ -311,7 +329,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateShowForm, rs.State) + require.EqualValues(t, flow.StateShowForm, rs.State) checkCredentials(t, true, users[agent].ID, provider, "hackerman+github+"+testID, false) @@ -340,19 +358,19 @@ func TestSettingsStrategy(t *testing.T) { }) t.Run("suite=link", func(t *testing.T) { - var link = func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + link := func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { req = nprSDK(t, agents[agent], "", time.Hour) body, res = testhelpers.HTTPPostForm(t, agents[agent], action(req), &url.Values{"csrf_token": {x.FakeCSRFToken}, "link": {provider}}) return } - var linkInvalid = func(agent, provider string) func(t *testing.T) { + linkInvalid := func(agent, provider string) func(t *testing.T) { return func(t *testing.T) { body, res, req := link(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) - //assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) + // assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), publicTS.URL+settings.RouteSubmitFlow+"?flow=") // The original options to link google and github are still there @@ -427,7 +445,7 @@ func TestSettingsStrategy(t *testing.T) { updatedFlowSDK, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(originalFlow.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateSuccess, updatedFlowSDK.State) + require.EqualValues(t, flow.StateSuccess, updatedFlowSDK.State) t.Run("flow=original", func(t *testing.T) { snapshotx.SnapshotTExcept(t, originalFlow.Ui.Nodes, []string{"0.attributes.value", "1.attributes.value"}) @@ -454,7 +472,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateSuccess, rs.State) + require.EqualValues(t, flow.StateSuccess, rs.State) snapshotx.SnapshotTExcept(t, rs.Ui.Nodes, []string{"0.attributes.value", "1.attributes.value"}) @@ -529,7 +547,7 @@ func TestSettingsStrategy(t *testing.T) { agent, provider := "githuber", "google" subject = "hackerman+new+google+" + testID - var runUnauthed = func(t *testing.T) *kratos.SettingsFlow { + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) time.Sleep(time.Millisecond) t.Cleanup(reset(t)) @@ -538,7 +556,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateShowForm, rs.State) + require.EqualValues(t, flow.StateShowForm, rs.State) checkCredentials(t, false, users[agent].ID, provider, subject, true) @@ -675,10 +693,12 @@ func TestPopulateSettingsMethod(t *testing.T) { oidc.NewUnlinkNode("google"), }, withpw: true, - i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ - "google:1234", + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + }, + Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`), }, - Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`)}, }, { c: defaultConfig, @@ -688,11 +708,13 @@ func TestPopulateSettingsMethod(t *testing.T) { oidc.NewUnlinkNode("google"), oidc.NewUnlinkNode("facebook"), }, - i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ - "google:1234", - "facebook:1234", + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + "facebook:1234", + }, + Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), }, - Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`)}, }, } { t.Run("iteration="+strconv.Itoa(k), func(t *testing.T) { diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 30010b80eb54..7a56ebaf45d4 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -51,7 +51,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return nil, err } diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 5fa174f48cbc..b49ff630e458 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -78,7 +78,7 @@ func (s *Strategy) decode(p *UpdateRegistrationFlowWithPasswordMethod, r *http.R } func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return err } diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index dacdef8c9ff2..1587888e0168 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -50,8 +50,8 @@ func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { } func TestRegistration(t *testing.T) { - ctx := context.Background() + t.Run("case=registration", func(t *testing.T) { reg := newRegistrationRegistry(t) conf := reg.Config() diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index 45e6bc045650..4ba0b115e53a 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -75,7 +75,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) } @@ -109,7 +109,7 @@ func (s *Strategy) continueSettingsFlow( w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasswordMethod, ) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go index 1e64544edee1..2993c428b701 100644 --- a/selfservice/strategy/password/strategy.go +++ b/selfservice/strategy/password/strategy.go @@ -26,9 +26,11 @@ import ( "github.com/ory/kratos/x" ) -var _ login.Strategy = new(Strategy) -var _ registration.Strategy = new(Strategy) -var _ identity.ActiveCredentialsCounter = new(Strategy) +var ( + _ login.Strategy = new(Strategy) + _ registration.Strategy = new(Strategy) + _ identity.ActiveCredentialsCounter = new(Strategy) +) type registrationStrategyDependencies interface { x.LoggingProvider diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go index 5c4a68be9a78..e94d779aef61 100644 --- a/selfservice/strategy/profile/strategy.go +++ b/selfservice/strategy/profile/strategy.go @@ -116,7 +116,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, &p, err) } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { return ctxUpdate, err } @@ -144,7 +144,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. } func (s *Strategy) continueFlow(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithProfileMethod) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go index bb09a2b925a0..f67407fe799f 100644 --- a/selfservice/strategy/profile/strategy_test.go +++ b/selfservice/strategy/profile/strategy_test.go @@ -32,6 +32,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/x" "github.com/ory/x/assertx" @@ -189,7 +190,7 @@ func TestStrategyTraits(t *testing.T) { t.Run("description=hydrate the proper fields", func(t *testing.T) { setPrivileged(t) - var run = func(t *testing.T, id *identity.Identity, payload *kratos.SettingsFlow, route string) { + run := func(t *testing.T, id *identity.Identity, payload *kratos.SettingsFlow, route string) { assert.NotEmpty(t, payload.Identity) assert.Equal(t, id.ID.String(), string(payload.Identity.Id)) assert.JSONEq(t, string(id.Traits), x.MustEncodeJSON(t, payload.Identity.Traits)) @@ -230,7 +231,7 @@ func TestStrategyTraits(t *testing.T) { }) }) - var expectValidationError = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + expectValidationError := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String())) @@ -239,7 +240,7 @@ func TestStrategyTraits(t *testing.T) { t.Run("description=should come back with form errors if some profile data is invalid", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.NotEmpty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", actual) assert.Equal(t, "too-short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual) assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) @@ -247,7 +248,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "length must be >= 25, but got 9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual) } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", "profile") v.Set("traits.should_long_string", "too-short") v.Set("traits.stringy", "bazbar") @@ -298,7 +299,7 @@ func TestStrategyTraits(t *testing.T) { }) t.Run("description=should end up at the login endpoint if trying to update protected field without sudo mode", func(t *testing.T) { - var run = func(t *testing.T, config *kratos.SettingsFlow, isAPI bool, c *http.Client) *http.Response { + run := func(t *testing.T, config *kratos.SettingsFlow, isAPI bool, c *http.Client) *http.Response { time.Sleep(time.Millisecond) values := testhelpers.SDKFormFieldsToURLValues(config.Ui.Nodes) @@ -343,7 +344,7 @@ func TestStrategyTraits(t *testing.T) { defer res.Body.Close() assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body) - assert.EqualValues(t, settings.StateSuccess, gjson.GetBytes(body, "state").String(), "%s", body) + assert.EqualValues(t, flow.StateSuccess, gjson.GetBytes(body, "state").String(), "%s", body) }) }) }) @@ -351,14 +352,14 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=fail first update", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) assert.Equal(t, "1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual) assert.Equal(t, "must be >= 1200 but found 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual) assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.should_big_number", "1") } @@ -379,8 +380,8 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=fail second update", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual) @@ -394,7 +395,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Del("traits.should_big_number") v.Set("traits.should_long_string", "short") @@ -414,7 +415,7 @@ func TestStrategyTraits(t *testing.T) { }) }) - var expectSuccess = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + expectSuccess := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, http.StatusOK, testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String())) @@ -423,8 +424,8 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=succeed with final request", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual) @@ -435,7 +436,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "this is such a long string, amazing stuff!", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").Value(), "%s", actual) } - var payload = func(newEmail string) func(v url.Values) { + payload := func(newEmail string) func(v url.Values) { return func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.email", newEmail) @@ -463,11 +464,11 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=try another update with invalid data", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.should_long_string", "short") } @@ -526,8 +527,8 @@ func TestStrategyTraits(t *testing.T) { conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, settings.StrategyProfile), nil) }) - var check = func(t *testing.T, actual, newEmail string) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual, newEmail string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual) m, err := reg.CourierPersister().LatestQueuedMessage(context.Background()) @@ -535,7 +536,7 @@ func TestStrategyTraits(t *testing.T) { assert.Contains(t, m.Subject, "verify your email address") } - var payload = func(newEmail string) func(v url.Values) { + payload := func(newEmail string) func(v url.Values) { return func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.email", newEmail) @@ -564,8 +565,8 @@ func TestStrategyTraits(t *testing.T) { t.Run("description=should update protected field with sudo mode", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, newEmail string, actual string) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, newEmail string, actual string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual) @@ -573,7 +574,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here } - var payload = func(email string) func(v url.Values) { + payload := func(email string) func(v url.Values) { return func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.email", email) diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go index e3840eba1b9b..bc9816d265f3 100644 --- a/selfservice/strategy/totp/login.go +++ b/selfservice/strategy/totp/login.go @@ -90,7 +90,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return nil, err } diff --git a/selfservice/strategy/totp/settings.go b/selfservice/strategy/totp/settings.go index 928c288be365..0587e87e2d6a 100644 --- a/selfservice/strategy/totp/settings.go +++ b/selfservice/strategy/totp/settings.go @@ -94,10 +94,10 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. if p.UnlinkTOTP { // This is a submit so we need to manually set the type to TOTP p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) } - } else if err := flow.MethodEnabledAndAllowedFromRequest(r, s.SettingsStrategyID(), s.d); err != nil { + } else if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) } @@ -127,7 +127,7 @@ func (s *Strategy) continueSettingsFlow( w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithTotpMethod, ) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } diff --git a/selfservice/strategy/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go index b9b294ffdf4e..0fd479f1b220 100644 --- a/selfservice/strategy/totp/settings_test.go +++ b/selfservice/strategy/totp/settings_test.go @@ -148,7 +148,7 @@ func TestCompleteSettings(t *testing.T) { }) id, _, key := createIdentity(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("totp_unlink", "true") } @@ -190,7 +190,7 @@ func TestCompleteSettings(t *testing.T) { }) id := createIdentityWithoutTOTP(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.TOTPCode, "111111") } @@ -225,7 +225,7 @@ func TestCompleteSettings(t *testing.T) { }) t.Run("type=unlink TOTP device", func(t *testing.T) { - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("totp_unlink", "true") } @@ -239,7 +239,7 @@ func TestCompleteSettings(t *testing.T) { actual, res := doAPIFlow(t, payload, id) assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) }) @@ -248,7 +248,7 @@ func TestCompleteSettings(t *testing.T) { actual, res := doBrowserFlow(t, true, payload, id) assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) }) @@ -257,13 +257,13 @@ func TestCompleteSettings(t *testing.T) { actual, res := doBrowserFlow(t, false, payload, id) assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), uiTS.URL) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) }) }) t.Run("type=set up TOTP device but code is incorrect", func(t *testing.T) { - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.TOTPCode, "111111") } @@ -332,10 +332,10 @@ func TestCompleteSettings(t *testing.T) { if isAPI || isSPA { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) } actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id)) diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index 815e99cb456b..a12f8fc0e182 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -82,5 +82,6 @@ ] }, "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "choose_method" } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index 815e99cb456b..a12f8fc0e182 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -82,5 +82,6 @@ ] }, "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "choose_method" } diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 58cf7b0f25a1..200fdcea2280 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -211,7 +211,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, flow.ErrStrategyNotResponsible } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleLoginError(r, f, err) } diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go index fab8e2cb77c5..565125319634 100644 --- a/selfservice/strategy/webauthn/registration.go +++ b/selfservice/strategy/webauthn/registration.go @@ -113,7 +113,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return s.handleRegistrationError(w, r, f, &p, err) } diff --git a/selfservice/strategy/webauthn/settings.go b/selfservice/strategy/webauthn/settings.go index ad00289c010a..51fbcf0f5adb 100644 --- a/selfservice/strategy/webauthn/settings.go +++ b/selfservice/strategy/webauthn/settings.go @@ -112,7 +112,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. if len(p.Register+p.Remove) > 0 { // This method has only two submit buttons p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) } } else { @@ -146,7 +146,7 @@ func (s *Strategy) continueSettingsFlow( ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod, ) error { if len(p.Register+p.Remove) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return err } diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index 04f571e7ab5e..27413df38b16 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -334,7 +334,7 @@ func TestCompleteSettings(t *testing.T) { } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) require.NoError(t, err) @@ -386,7 +386,7 @@ func TestCompleteSettings(t *testing.T) { } t.Run("response", func(t *testing.T) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateShowForm, gjson.Get(body, "state").String(), body) snapshotx.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes.#(attributes.name==webauthn_remove)").String()), nil) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) @@ -426,7 +426,7 @@ func TestCompleteSettings(t *testing.T) { } t.Run("response", func(t *testing.T) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) require.NoError(t, err) _, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) @@ -463,7 +463,7 @@ func TestCompleteSettings(t *testing.T) { } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) } actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) @@ -496,7 +496,7 @@ func TestCompleteSettings(t *testing.T) { } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(body, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(body, "state").String())) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) require.NoError(t, err) diff --git a/spec/api.json b/spec/api.json old mode 100755 new mode 100644 index d1973e17f7fd..005fef8d643d --- a/spec/api.json +++ b/spec/api.json @@ -81,6 +81,9 @@ } }, "schemas": { + "CodeAddressType": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -906,6 +909,18 @@ }, "type": "object" }, + "identityCredentialsCode": { + "description": "CredentialsCode represents a one time login/registration code", + "properties": { + "address_type": { + "$ref": "#/components/schemas/CodeAddressType" + }, + "used_at": { + "$ref": "#/components/schemas/NullTime" + } + }, + "type": "object" + }, "identityCredentialsOidc": { "properties": { "providers": { @@ -956,7 +971,8 @@ "totp", "oidc", "webauthn", - "lookup_secret" + "lookup_secret", + "code" ], "title": "CredentialsType represents several different credential types, like password credentials, passwordless credentials,", "type": "string" @@ -1209,6 +1225,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the login flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -1227,11 +1246,21 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "title": "Login Flow", "type": "object" }, + "loginFlowState": { + "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "Login Flow State" + }, "logoutFlow": { "description": "Logout Flow", "properties": { @@ -1285,7 +1314,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "enum": [ "recovery_invalid", "recovery_valid", @@ -1296,10 +1325,12 @@ "verification_code_invalid", "verification_code_valid", "otp", - "stub" + "stub", + "login_code_valid", + "registration_code_valid" ], "type": "string", - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/components/schemas/courierMessageType" @@ -1504,7 +1535,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/recoveryFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -1532,8 +1563,7 @@ "sent_email", "passed_challenge" ], - "title": "Recovery Flow State", - "type": "string" + "title": "Recovery Flow State" }, "recoveryIdentityAddress": { "properties": { @@ -1623,6 +1653,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed." + }, "transient_payload": { "description": "TransientPayload is used to pass data from the registration to a webhook", "type": "object" @@ -1640,10 +1673,20 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "type": "object" }, + "registrationFlowState": { + "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "State represents the state of this request:" + }, "selfServiceFlowExpiredError": { "description": "Is sent when a flow is expired", "properties": { @@ -1829,7 +1872,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/settingsFlowState" + "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -1857,8 +1900,7 @@ "show_form", "success" ], - "title": "State represents the state of this flow. It knows two states:", - "type": "string" + "title": "State represents the state of this flow. It knows two states:" }, "successfulCodeExchangeResponse": { "description": "The Response for Registration Flows via API", @@ -2358,6 +2400,7 @@ "updateLoginFlowBody": { "discriminator": { "mapping": { + "code": "#/components/schemas/updateLoginFlowWithCodeMethod", "lookup_secret": "#/components/schemas/updateLoginFlowWithLookupSecretMethod", "oidc": "#/components/schemas/updateLoginFlowWithOidcMethod", "password": "#/components/schemas/updateLoginFlowWithPasswordMethod", @@ -2381,9 +2424,42 @@ }, { "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" + }, + { + "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" } ] }, + "updateLoginFlowWithCodeMethod": { + "description": "Update Login flow using the code method", + "properties": { + "code": { + "description": "Code is the 6 digits code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "identifier": { + "description": "Identifier is the code identifier\nThe identifier requires that the user has already completed the registration or settings with code flow.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"code\" when logging in using the code strategy.", + "type": "string" + }, + "resend": { + "description": "Resend is set when the user wants to resend the code", + "type": "string" + } + }, + "required": [ + "method", + "csrf_token" + ], + "type": "object" + }, "updateLoginFlowWithLookupSecretMethod": { "description": "Update Login Flow with Lookup Secret Method", "properties": { @@ -2594,6 +2670,7 @@ "description": "Update Registration Request Body", "discriminator": { "mapping": { + "code": "#/components/schemas/updateRegistrationFlowWithCodeMethod", "oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod", "password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod", "webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" @@ -2609,9 +2686,46 @@ }, { "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + }, + { + "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" } ] }, + "updateRegistrationFlowWithCodeMethod": { + "description": "Update Registration Flow with Code Method", + "properties": { + "code": { + "description": "The OTP Code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "The CSRF Token", + "type": "string" + }, + "method": { + "description": "Method to use\n\nThis field must be set to `code` when using the code method.", + "type": "string" + }, + "resend": { + "description": "Resend restarts the flow with a new code", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + }, + "required": [ + "traits", + "method" + ], + "type": "object" + }, "updateRegistrationFlowWithOidcMethod": { "description": "Update Registration Flow with OpenID Connect Method", "properties": { @@ -3064,7 +3178,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/verificationFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -3089,8 +3203,7 @@ "sent_email", "passed_challenge" ], - "title": "Verification Flow State", - "type": "string" + "title": "Verification Flow State" }, "version": { "properties": { @@ -3549,7 +3662,8 @@ "totp", "oidc", "webauthn", - "lookup_secret" + "lookup_secret", + "code" ], "type": "string" }, diff --git a/spec/swagger.json b/spec/swagger.json index 01ad8fb3bf27..368785b299a8 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3076,6 +3076,9 @@ } }, "definitions": { + "CodeAddressType": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -3090,6 +3093,20 @@ "type": "object", "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, + "NullTime": { + "description": "NullTime implements the Scanner interface so\nit can be used as a scan destination, similar to NullString.", + "type": "object", + "title": "NullTime represents a time.Time that may be null.", + "properties": { + "Time": { + "type": "string", + "format": "date-time" + }, + "Valid": { + "type": "boolean" + } + } + }, "OAuth2Client": { "type": "object", "title": "OAuth2Client OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.", @@ -3847,6 +3864,18 @@ } } }, + "identityCredentialsCode": { + "description": "CredentialsCode represents a one time login/registration code", + "type": "object", + "properties": { + "address_type": { + "$ref": "#/definitions/CodeAddressType" + }, + "used_at": { + "$ref": "#/definitions/NullTime" + } + } + }, "identityCredentialsOidc": { "type": "object", "title": "CredentialsOIDC is contains the configuration for credentials of the type oidc.", @@ -4098,7 +4127,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "properties": { "active": { @@ -4150,6 +4180,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the login flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -4163,6 +4196,10 @@ } } }, + "loginFlowState": { + "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", + "title": "Login Flow State" + }, "logoutFlow": { "description": "Logout Flow", "type": "object", @@ -4229,7 +4266,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "type": "string", "enum": [ "recovery_invalid", @@ -4241,9 +4278,11 @@ "verification_code_invalid", "verification_code_valid", "otp", - "stub" + "stub", + "login_code_valid", + "registration_code_valid" ], - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/definitions/courierMessageType" @@ -4437,7 +4476,7 @@ "type": "string" }, "state": { - "$ref": "#/definitions/recoveryFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, "type": { "$ref": "#/definitions/selfServiceFlowType" @@ -4449,7 +4488,6 @@ }, "recoveryFlowState": { "description": "The state represents the state of the recovery flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "type": "string", "title": "Recovery Flow State" }, "recoveryIdentityAddress": { @@ -4509,7 +4547,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "properties": { "active": { @@ -4549,6 +4588,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed." + }, "transient_payload": { "description": "TransientPayload is used to pass data from the registration to a webhook", "type": "object" @@ -4561,6 +4603,10 @@ } } }, + "registrationFlowState": { + "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", + "title": "State represents the state of this request:" + }, "selfServiceFlowExpiredError": { "description": "Is sent when a flow is expired", "type": "object", @@ -4747,7 +4793,7 @@ "type": "string" }, "state": { - "$ref": "#/definitions/settingsFlowState" + "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, "type": { "$ref": "#/definitions/selfServiceFlowType" @@ -4759,7 +4805,6 @@ }, "settingsFlowState": { "description": "show_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent.", - "type": "string", "title": "State represents the state of this flow. It knows two states:" }, "successfulCodeExchangeResponse": { @@ -5235,6 +5280,36 @@ "updateLoginFlowBody": { "type": "object" }, + "updateLoginFlowWithCodeMethod": { + "description": "Update Login flow using the code method", + "type": "object", + "required": [ + "method", + "csrf_token" + ], + "properties": { + "code": { + "description": "Code is the 6 digits code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "identifier": { + "description": "Identifier is the code identifier\nThe identifier requires that the user has already completed the registration or settings with code flow.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"code\" when logging in using the code strategy.", + "type": "string" + }, + "resend": { + "description": "Resend is set when the user wants to resend the code", + "type": "string" + } + } + }, "updateLoginFlowWithLookupSecretMethod": { "description": "Update Login Flow with Lookup Secret Method", "type": "object", @@ -5431,6 +5506,40 @@ "description": "Update Registration Request Body", "type": "object" }, + "updateRegistrationFlowWithCodeMethod": { + "description": "Update Registration Flow with Code Method", + "type": "object", + "required": [ + "traits", + "method" + ], + "properties": { + "code": { + "description": "The OTP Code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "The CSRF Token", + "type": "string" + }, + "method": { + "description": "Method to use\n\nThis field must be set to `code` when using the code method.", + "type": "string" + }, + "resend": { + "description": "Resend restarts the flow with a new code", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, "updateRegistrationFlowWithOidcMethod": { "description": "Update Registration Flow with OpenID Connect Method", "type": "object", @@ -5844,7 +5953,7 @@ "type": "string" }, "state": { - "$ref": "#/definitions/verificationFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, "type": { "$ref": "#/definitions/selfServiceFlowType" @@ -5856,7 +5965,6 @@ }, "verificationFlowState": { "description": "The state represents the state of the verification flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "type": "string", "title": "Verification Flow State" }, "version": { diff --git a/test/e2e/.go-version b/test/e2e/.go-version new file mode 100644 index 000000000000..6681c8c19ab4 --- /dev/null +++ b/test/e2e/.go-version @@ -0,0 +1 @@ +1.19.8 diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts new file mode 100644 index 000000000000..46d8854c4e99 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -0,0 +1,176 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Login error messages with code method", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "code", + }, + { + route: react.login, + app: "react" as "react", + profile: "code", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.deleteMail() + cy.proxy(app) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + + const email = gen.email() + cy.wrap(email).as("email") + cy.registerWithCode({ email }) + + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should show error message when account identifier does not exist", () => { + const email = gen.email() + + cy.get('input[name="identifier"]').type(email) + cy.submitCodeForm() + + cy.url().should("contain", "login") + + cy.get('[data-testid="ui/message/4000035"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", + ) + }) + + it("should show error message when code is invalid", () => { + cy.get("@email").then((email) => { + cy.get('input[name="identifier"]').clear().type(email.toString()) + }) + + cy.submitCodeForm() + + cy.url().should("contain", "login") + cy.get('[data-testid="ui/message/1010014"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.get('input[name="code"]').type("invalid-code") + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4010008"]').should( + "contain", + "The login code is invalid or has already been used. Please try again.", + ) + }) + + it("should show error message when identifier has changed", () => { + cy.get("@email").then((email) => { + cy.get('input[name="identifier"]').type(email.toString()) + }) + + cy.submitCodeForm() + + cy.url().should("contain", "login") + cy.get('input[name="identifier"]').clear().type(gen.email()) + cy.get('input[name="code"]').type("invalid-code") + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000035"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", + ) + }) + + it("should show error message when required fields are missing", () => { + cy.get("@email").then((email) => { + cy.get('input[name="identifier"]').type(email.toString()) + }) + + cy.submitCodeForm() + cy.url().should("contain", "login") + + cy.removeAttribute(['input[name="code"]'], "required") + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property code is missing", + ) + + cy.get('input[name="code"]').type("invalid-code") + cy.removeAttribute(['input[name="identifier"]'], "required") + + cy.get('input[name="identifier"]').clear() + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property identifier is missing", + ) + }) + + it("should show error message when code is expired", () => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code = { + passwordless_enabled: true, + config: { + lifespan: "1ns", + }, + } + return config + }).then(() => { + cy.visit(route) + }) + + cy.get("@email").then((email) => { + cy.get('input[name="identifier"]').type(email.toString()) + }) + cy.submitCodeForm() + + cy.url().should("contain", "login") + + cy.get("@email").then((email) => { + cy.getLoginCodeFromEmail(email.toString()).should((code) => { + cy.get('input[name="code"]').type(code) + }) + }) + + cy.submitCodeForm() + + // the react app does not show the error message for 410 errors + // it just creates a new flow + if (app === "express") { + cy.get('[data-testid="ui/message/4010001"]').should( + "contain", + "The login flow expired", + ) + } else { + cy.get("input[name=identifier]").should("be.visible") + } + + cy.noSession() + + cy.updateConfigFile((config) => { + config.selfservice.methods.code = { + passwordless_enabled: true, + config: { + lifespan: "1h", + }, + } + return config + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts new file mode 100644 index 000000000000..83cbbb605c20 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -0,0 +1,164 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Login success with code method", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "code", + }, + { + route: react.login, + app: "react" as "react", + profile: "code", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + cy.setPostCodeRegistrationHooks([]) + cy.setupHooks("login", "after", "code", []) + }) + + beforeEach(() => { + const email = gen.email() + cy.wrap(email).as("email") + cy.registerWithCode({ email }) + + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should be able to sign in with code", () => { + cy.get("@email").then((email) => { + cy.get('input[name="identifier"]').clear().type(email.toString()) + cy.submitCodeForm() + + cy.getLoginCodeFromEmail(email.toString()).should((code) => { + cy.get('input[name="code"]').type(code) + + cy.get("button[name=method][value=code]").click() + }) + + cy.location("pathname").should("not.contain", "login") + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + + it("should be able to resend login code", () => { + cy.get("@email").then((email) => { + cy.get('input[name="identifier"]').clear().type(email.toString()) + cy.submitCodeForm() + + cy.getLoginCodeFromEmail(email.toString()).should((code) => { + cy.wrap(code).as("code1") + }) + + cy.get("button[name=resend]").click() + + cy.getLoginCodeFromEmail(email.toString()).should((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code1").then((code1) => { + cy.get("@code2").then((code2) => { + expect(code1).to.not.equal(code2) + }) + }) + + // attempt to submit code 1 + cy.get("@code1").then((code1) => { + cy.get('input[name="code"]').clear().type(code1.toString()) + }) + + cy.get("button[name=method][value=code]").click() + + cy.get("[data-testid='ui/message/4010008']").contains( + "The login code is invalid or has already been used", + ) + + // attempt to submit code 2 + cy.get("@code2").then((code2) => { + cy.get('input[name="code"]').clear().type(code2.toString()) + }) + + cy.get('button[name="method"][value="code"]').click() + + if (app === "express") { + cy.get('a[href*="sessions"').click() + } + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + + it("should be able to login to un-verfied email", () => { + const email = gen.email() + const email2 = gen.email() + + // Setup complex schema + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.complex.traits.schema.json", + ) + + cy.registerWithCode({ + email: email, + traits: { + "traits.username": Math.random().toString(36), + "traits.email2": email2, + }, + }) + + // There are verification emails from the registration process in the inbox that we need to deleted + // for the assertions below to pass. + cy.deleteMail({ atLeast: 1 }) + + cy.visit(route) + + cy.get('input[name="identifier"]').clear().type(email2) + cy.submitCodeForm() + + cy.getLoginCodeFromEmail(email2).should((code) => { + cy.get('input[name="code"]').type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + expect(session.identity.verifiable_addresses).to.have.length(2) + expect(session.identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(session.identity.verifiable_addresses[1].status).to.equal( + "completed", + ) + }, + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts new file mode 100644 index 000000000000..a4d6596008dd --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration error messages with code method", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "code", + }, + { + route: react.registration, + app: "react" as "react", + profile: "code", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.proxy(app) + cy.useConfigProfile(profile) + cy.deleteMail() + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should show error message when code is invalid", () => { + const email = gen.email() + + cy.get('input[name="traits.email"]').type(email) + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.get('input[name="code"]').type("invalid-code") + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4040003"]').should( + "contain", + "The registration code is invalid or has already been used. Please try again.", + ) + }) + + it("should show error message when traits have changed", () => { + const email = gen.email() + + cy.get('input[name="traits.email"]').type(email) + cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.get('input[name="traits.email"]') + .clear() + .type("changed-email@email.com") + cy.get('input[name="code"]').type("invalid-code") + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000036"]').should( + "contain", + "The provided traits do not match the traits previously associated with this flow.", + ) + }) + + it("should show error message when required fields are missing", () => { + const email = gen.email() + + cy.get('input[name="traits.email"]').type(email) + cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.removeAttribute(['input[name="code"]'], "required") + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property code is missing", + ) + + cy.get('input[name="traits.email"]').clear() + cy.get('input[name="code"]').type("invalid-code") + cy.removeAttribute(['input[name="traits.email"]'], "required") + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property email is missing", + ) + }) + + it("should show error message when code is expired", () => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1ns" + return config + }) + cy.visit(route) + + const email = gen.email() + cy.get('input[name="traits.email"]').type(email) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get('input[name="code"]').type(code) + cy.submitCodeForm() + }) + + // in the react spa app we don't show the 410 gone error. we create a new flow. + if (app === "express") { + cy.get('[data-testid="ui/message/4040001"]').should( + "contain", + "The registration flow expired", + ) + } else { + cy.get('input[name="traits.email"]').should("be.visible") + } + + cy.noSession() + + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1h" + return config + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts new file mode 100644 index 000000000000..299d5707a7d5 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -0,0 +1,241 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration success with code method", () => { + ;[ + { + route: express.registration, + login: express.login, + recovery: express.recovery, + app: "express" as "express", + profile: "code", + }, + { + route: react.registration, + login: react.login, + recovery: react.recovery, + app: "react" as "react", + profile: "code", + }, + ].forEach(({ route, login, recovery, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should be able to resend the registration code", async () => { + const email = gen.email() + + cy.get(`input[name='traits.email']`).type(email) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => + cy.wrap(code).as("code1"), + ) + + cy.get(`input[name='traits.email']`).should("have.value", email) + cy.get(`input[name='method'][value='code'][type='hidden']`).should( + "exist", + ) + cy.get(`button[name='resend'][value='code']`).click() + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code1").then((code1) => { + // previous code should not work + cy.get('input[name="code"]').clear().type(code1.toString()) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/4040003"]').should( + "contain.text", + "The registration code is invalid or has already been used. Please try again.", + ) + }) + + cy.get("@code2").then((code2) => { + cy.get('input[name="code"]').clear().type(code2.toString()) + cy.submitCodeForm() + }) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should sign up and be logged in with session hook", () => { + const email = gen.email() + + cy.get(` input[name='traits.email']`).type(email) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(`input[name=code]`).type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to sign up without session hook", () => { + cy.setPostCodeRegistrationHooks([]) + const email = gen.email() + + cy.get(`input[name='traits.email']`).type(email) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(`input[name=code]`).type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.visit(login) + cy.get(`input[name=identifier]`).type(email) + cy.get("button[name=method][value=code]").click() + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get(`input[name=code]`).type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to recover account when registered with code", () => { + const email = gen.email() + cy.registerWithCode({ email }) + + cy.clearAllCookies() + cy.visit(recovery) + + cy.get('input[name="email"]').type(email) + cy.get('button[name="method"][value="code"]').click() + + cy.recoveryEmailWithCode({ expect: { email } }) + cy.get('button[value="code"]').click() + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + // Try keep this test as the last one, as it updates the identity schema. + it("should be able to use multiple identifiers to signup with and sign in to", () => { + cy.setPostCodeRegistrationHooks([ + { + hook: "session", + }, + ]) + + // Setup complex schema + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.complex.traits.schema.json", + ) + + cy.visit(route) + + cy.get(`input[name='traits.username']`).type(Math.random().toString(36)) + + const email = gen.email() + cy.get(`input[name='traits.email']`).type(email) + + const email2 = gen.email() + cy.get(`input[name='traits.email2']`).type(email2) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + // intentionally use email 1 to sign up for the account + cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).should( + (code) => { + cy.get(`input[name=code]`).type(code) + cy.get("button[name=method][value=code]").click() + }, + ) + + cy.logout() + + // There are verification emails from the registration process in the inbox that we need to deleted + // for the assertions below to pass. + cy.deleteMail({ atLeast: 1 }) + + // Attempt to sign in with email 2 (should fail) + cy.visit(login) + cy.get(`input[name=identifier]`).type(email2) + + cy.get("button[name=method][value=code]").click() + + cy.getLoginCodeFromEmail(email2, { + expectedCount: 1, + }).should((code) => { + cy.get(`input[name=code]`).type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(2) + expect( + identity.verifiable_addresses.filter((v) => v.value === email)[0] + .status, + ).to.equal("completed") + expect( + identity.verifiable_addresses.filter((v) => v.value === email2)[0] + .status, + ).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 9ab3ea5a41ff..d924827b5881 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -18,6 +18,7 @@ import dayjs from "dayjs" import YAML from "yamljs" import { Strategy } from "." import { OryKratosConfiguration } from "./config" +import { UiNode, UiNodeAttributes } from "@ory/kratos-client" const configFile = "kratos.generated.yml" @@ -221,6 +222,10 @@ Cypress.Commands.add("setPostPasswordRegistrationHooks", (hooks) => { cy.setupHooks("registration", "after", "password", hooks) }) +Cypress.Commands.add("setPostCodeRegistrationHooks", (hooks) => { + cy.setupHooks("registration", "after", "code", hooks) +}) + Cypress.Commands.add("shortLoginLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.login.lifespan = "100ms" @@ -377,6 +382,83 @@ Cypress.Commands.add( }, ) +Cypress.Commands.add( + "registerWithCode", + ({ + email = gen.email(), + code = undefined, + traits = {}, + query = {}, + expectedMailCount = 1, + } = {}) => { + cy.clearAllCookies() + + cy.request({ + url: APP_URL + "/self-service/registration/browser", + method: "GET", + followRedirect: false, + headers: { + Accept: "application/json", + }, + qs: query || {}, + }).then(({ body, status }) => { + expect(status).to.eq(200) + const form = body.ui + return cy + .request({ + headers: { + Accept: "application/json", + }, + method: form.method, + body: mergeFields(form, { + method: "code", + "traits.email": email, + ...traits, + ...(code && { code }), + }), + url: form.action, + followRedirect: false, + failOnStatusCode: false, + }) + .then(({ body }) => { + if (!code) { + expect( + body.ui.nodes.find( + (f: UiNode) => + f.group === "code" && + "name" in f.attributes && + f.attributes.name === "traits.email", + ).attributes.value, + ).to.eq(email) + + return cy + .getRegistrationCodeFromEmail(email, { + expectedCount: expectedMailCount, + }) + .then((code) => { + return cy.request({ + headers: { + Accept: "application/json", + }, + method: form.method, + body: mergeFields(form, { + method: "code", + "traits.email": email, + code, + ...traits, + }), + url: form.action, + followRedirect: false, + }) + }) + } else { + expect(body.session).to.contain(email) + } + }) + }) + }, +) + Cypress.Commands.add( "registerApi", ({ email = gen.email(), password = gen.password(), fields = {} } = {}) => @@ -1105,7 +1187,7 @@ Cypress.Commands.add( Cypress.Commands.add( "verifyEmailButExpired", ({ expect: { email }, strategy = "code" }) => { - cy.getMail().then((message) => { + cy.getMail().should((message) => { expect(message.subject).to.equal("Please verify your email address") expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh") @@ -1171,30 +1253,51 @@ Cypress.Commands.add("expectSettingsSaved", () => { ) }) -Cypress.Commands.add("getMail", ({ removeMail = true } = {}) => { - let tries = 0 - const req = () => - cy.request(`${MAIL_API}/mail`).then((response) => { - expect(response.body).to.have.property("mailItems") - const count = response.body.mailItems.length - if (count === 0 && tries < 100) { - tries++ - cy.wait(pollInterval) - return req() - } +Cypress.Commands.add( + "getMail", + ({ removeMail = true, expectedCount = 1, email = undefined } = {}) => { + let tries = 0 + const req = () => + cy.request(`${MAIL_API}/mail`).then((response) => { + expect(response.body).to.have.property("mailItems") + let count = response.body.mailItems.length + if (count === 0 && tries < 100) { + tries++ + cy.wait(pollInterval) + return req() + } - expect(count).to.equal(1) - if (removeMail) { - return cy - .deleteMail({ atLeast: count }) - .then(() => Promise.resolve(response.body.mailItems[0])) - } + let mailItem: any + if (email) { + const filtered = response.body.mailItems.filter((m: any) => + m.toAddresses.includes(email), + ) - return Promise.resolve(response.body.mailItems[0]) - }) + if (filtered.length === 0) { + tries++ + cy.wait(pollInterval) + return req() + } - return req() -}) + expect(filtered.length).to.equal(expectedCount) + mailItem = filtered[0] + } else { + expect(count).to.equal(expectedCount) + mailItem = response.body.mailItems[0] + } + + if (removeMail) { + return cy.deleteMail({ atLeast: count }).then(() => { + return Promise.resolve(mailItem) + }) + } + + return Promise.resolve(mailItem) + }) + + return req() + }, +) Cypress.Commands.add("clearAllCookies", () => { cy.clearCookies({ domain: null }) @@ -1210,6 +1313,11 @@ Cypress.Commands.add("submitProfileForm", () => { cy.get('[name="method"][value="profile"]:disabled').should("not.exist") }) +Cypress.Commands.add("submitCodeForm", () => { + cy.get('button[name="method"][value="code"]').click() + cy.get('button[name="method"][value="code"]:disabled').should("not.exist") +}) + Cypress.Commands.add("clickWebAuthButton", (type: string) => { cy.get('*[data-testid="node/script/webauthn_script"]').should("exist") cy.wait(500) // Wait for script to load @@ -1375,3 +1483,40 @@ Cypress.Commands.add("getVerificationCodeFromEmail", (email) => { return code }) }) + +Cypress.Commands.add("enableRegistrationViaCode", (enable: boolean = true) => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code.passwordless_enabled = enable + return config + }) +}) + +Cypress.Commands.add("getRegistrationCodeFromEmail", (email, opts) => { + return cy + .getMail({ removeMail: true, email, ...opts }) + .should((message) => { + expect(message.subject).to.equal("Complete your account registration") + expect(message.toAddresses[0].trim()).to.equal(email) + }) + .then((message) => { + const code = extractRecoveryCode(message.body) + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + return code + }) +}) + +Cypress.Commands.add("getLoginCodeFromEmail", (email, opts) => { + return cy + .getMail({ removeMail: true, email, ...opts }) + .should((message) => { + expect(message.subject).to.equal("Login to your account") + expect(message.toAddresses[0].trim()).to.equal(email) + }) + .then((message) => { + const code = extractRecoveryCode(message.body) + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + return code + }) +}) diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/cypress/support/config.d.ts index e2bb5294b91e..1bac43efd7b3 100644 --- a/test/e2e/cypress/support/config.d.ts +++ b/test/e2e/cypress/support/config.d.ts @@ -106,6 +106,8 @@ export type EnablesLinkMethod = boolean export type OverrideTheBaseURLWhichShouldBeUsedAsTheBaseForRecoveryAndVerificationLinks = string export type HowLongALinkIsValidFor = string +export type EnablesLoginWithCodeMethod = boolean +export type EnablesRegistrationWithCodeMethod = boolean export type EnablesCodeMethod = boolean export type HowLongACodeIsValidFor = string export type EnablesUsernameEmailAndPasswordMethod = boolean @@ -210,6 +212,7 @@ export type Provider = | "dingtalk" | "patreon" | "linkedin" + | "lark" export type OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons = string /** @@ -260,6 +263,20 @@ export type DataSourceName = string * You can override certain or all message templates by pointing this key to the path where the templates are located. */ export type OverrideMessageTemplates = string +/** + * Defines how emails will be sent, either through SMTP (default) or HTTP. + */ +export type DeliveryStrategy = "smtp" | "http" +/** + * This URL will be used to send the emails to. + */ +export type HTTPAddressOfAPIEndpoint = string +/** + * Define which auth mechanism to use for auth with the HTTP email provider + */ +export type AuthMechanisms = + | WebHookAuthApiKeyProperties + | WebHookAuthBasicAuthProperties /** * This URI will be used to connect to the SMTP server. Use the scheme smtps for implicit TLS sessions or smtp for explicit StartTLS/cleartext sessions. Please note that TLS is always enforced with certificate trust verification by default for security reasons on both schemes. With the smtp scheme you can use the query parameter (`?disable_starttls=true`) to allow cleartext sessions or (`?disable_starttls=false`) to enforce StartTLS (default behaviour). Additionally, use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates (default behaviour) on both implicit and explicit TLS sessions. */ @@ -291,17 +308,21 @@ export type SMSSenderAddress = string /** * This URL will be used to connect to the SMS provider. */ -export type HTTPAddressOfAPIEndpoint = string +export type HTTPAddressOfAPIEndpoint1 = string /** * Define which auth mechanism to use for auth with the SMS provider */ -export type AuthMechanisms = +export type AuthMechanisms1 = | WebHookAuthApiKeyProperties | WebHookAuthBasicAuthProperties /** * If set, the login and registration flows will handle the Ory OAuth 2.0 & OpenID `login_challenge` query parameter to serve as an OpenID Connect Provider. This URL should point to Ory Hydra when you are not running on the Ory Network and be left untouched otherwise. */ export type OAuth20ProviderURL = string +/** + * Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow. + */ +export type PersistOAuth2RequestBetweenFlows = boolean /** * Disable request logging for /health/alive and /health/ready endpoints */ @@ -506,6 +527,7 @@ export interface OryKratosConfiguration2 { config?: LinkConfiguration } code?: { + passwordless_enabled?: boolean enabled?: EnablesCodeMethod config?: CodeConfiguration } @@ -674,15 +696,23 @@ export interface SelfServiceAfterRegistration { password?: SelfServiceAfterRegistrationMethod webauthn?: SelfServiceAfterRegistrationMethod oidc?: SelfServiceAfterRegistrationMethod + code?: SelfServiceAfterRegistrationMethod hooks?: SelfServiceHooks } export interface SelfServiceAfterRegistrationMethod { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault - hooks?: (SelfServiceSessionIssuerHook | SelfServiceWebHook)[] + hooks?: ( + | SelfServiceSessionIssuerHook + | SelfServiceWebHook + | SelfServiceShowVerificationUIHook + )[] } export interface SelfServiceSessionIssuerHook { hook: "session" } +export interface SelfServiceShowVerificationUIHook { + hook: "show_verification_ui" +} export interface SelfServiceBeforeLogin { hooks?: SelfServiceHooks } @@ -691,6 +721,7 @@ export interface SelfServiceAfterLogin { password?: SelfServiceAfterDefaultLoginMethod webauthn?: SelfServiceAfterDefaultLoginMethod oidc?: SelfServiceAfterOIDCLoginMethod + code?: SelfServiceAfterDefaultLoginMethod hooks?: ( | SelfServiceWebHook | SelfServiceSessionRevokerHook @@ -868,6 +899,8 @@ export interface CourierConfiguration { * Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned */ message_retries?: number + delivery_strategy?: DeliveryStrategy + http?: HTTPConfiguration smtp: SMTPConfiguration sms?: SMSSenderConfiguration } @@ -892,6 +925,61 @@ export interface EmailCourierTemplate { } subject?: string } +/** + * Configures outgoing emails using HTTP. + */ +export interface HTTPConfiguration { + request_config?: HttpRequestConfig +} +export interface HttpRequestConfig { + url?: HTTPAddressOfAPIEndpoint + /** + * The HTTP method to use (GET, POST, etc). Defaults to POST. + */ + method?: string + /** + * The HTTP headers that must be applied to request + */ + headers?: { + [k: string]: string | undefined + } + /** + * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads + */ + body?: string + auth?: AuthMechanisms + additionalProperties?: false +} +export interface WebHookAuthApiKeyProperties { + type: "api_key" + config: { + /** + * The name of the api key + */ + name: string + /** + * The value of the api key + */ + value: string + /** + * How the api key should be transferred + */ + in: "header" | "cookie" + } +} +export interface WebHookAuthBasicAuthProperties { + type: "basic_auth" + config: { + /** + * user name for basic auth + */ + user: string + /** + * password for basic auth + */ + password: string + } +} /** * Configures outgoing emails using the SMTP protocol. */ @@ -920,7 +1008,7 @@ export interface SMSSenderConfiguration { enabled?: boolean from?: SMSSenderAddress request_config?: { - url: HTTPAddressOfAPIEndpoint + url: HTTPAddressOfAPIEndpoint1 /** * The HTTP method to use (GET, POST, etc). */ @@ -935,44 +1023,14 @@ export interface SMSSenderConfiguration { * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads */ body?: string - auth?: AuthMechanisms + auth?: AuthMechanisms1 additionalProperties?: false } } -export interface WebHookAuthApiKeyProperties { - type: "api_key" - config: { - /** - * The name of the api key - */ - name: string - /** - * The value of the api key - */ - value: string - /** - * How the api key should be transferred - */ - in: "header" | "cookie" - } -} -export interface WebHookAuthBasicAuthProperties { - type: "basic_auth" - config: { - /** - * user name for basic auth - */ - user: string - /** - * password for basic auth - */ - password: string - } -} export interface OAuth2ProviderConfiguration { url?: OAuth20ProviderURL headers?: HTTPRequestHeaders - override_return_to?: boolean + override_return_to?: PersistOAuth2RequestBetweenFlows } /** * These headers will be passed in HTTP request to the OAuth2 Provider. diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index c9a52a0c7e8e..47b66308c252 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -37,7 +37,7 @@ declare global { getSession(opts?: { expectAal?: "aal2" | "aal1" expectMethods?: Array< - "password" | "webauthn" | "lookup_secret" | "totp" + "password" | "webauthn" | "lookup_secret" | "totp" | "code" > }): Chainable @@ -70,6 +70,19 @@ declare global { fields?: { [key: string]: any } }): Chainable> + /** + * Register a user with a code + * + * @param opts + */ + registerWithCode(opts: { + email: string + code?: string + traits?: { [key: string]: any } + query?: { [key: string]: string } + expectedMailCount?: number + }): Chainable> + /** * Updates a user's settings using an API flow * @@ -89,7 +102,11 @@ declare global { * * @param opts */ - getMail(opts?: { removeMail: boolean }): Chainable + getMail(opts?: { + removeMail: boolean + expectedCount?: number + email?: string + }): Chainable performEmailVerification(opts?: { expect?: { email?: string; redirectTo?: string } @@ -166,7 +183,7 @@ declare global { | "verification" | "settings", phase: "before" | "after", - kind: "password" | "webauthn" | "oidc", + kind: "password" | "webauthn" | "oidc" | "code", hooks: Array<{ hook: string; config?: any }>, ): Chainable @@ -179,6 +196,15 @@ declare global { hooks: Array<{ hook: string; config?: any }>, ): Chainable + /** + * Sets the post code registration hook. + * + * @param hooks + */ + setPostCodeRegistrationHooks( + hooks: Array<{ hook: string; config?: any }>, + ): Chainable + /** * Submits a verification flow via the Browser * @@ -332,6 +358,11 @@ declare global { */ submitProfileForm(): Chainable + /** + * Submits a code form by clicking the button with method=code + */ + submitCodeForm(): Chainable + /** * Expect a CSRF error to occur * @@ -689,6 +720,28 @@ declare global { * Extracts a verification code from the received email */ getVerificationCodeFromEmail(email: string): Chainable + + /** + * Enables the registration code method + * @param enable + */ + enableRegistrationViaCode(enable: boolean): Chainable + + /** + * Extracts a registration code from the received email + */ + getRegistrationCodeFromEmail( + email: string, + opts?: { expectedCount: number; removeMail?: boolean }, + ): Chainable + + /** + * Extracts a login code from the received email + */ + getLoginCodeFromEmail( + email: string, + opts?: { expectedCount: number }, + ): Chainable } } } diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml new file mode 100644 index 000000000000..ec69fb050fab --- /dev/null +++ b/test/e2e/profiles/code/.kratos.yml @@ -0,0 +1,48 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + ui_url: http://localhost:4455/registration + after: + code: + hooks: + - + hook: session + + login: + ui_url: http://localhost:4455/login + after: + code: + hooks: + - + hook: require_verified_address + error: + ui_url: http://localhost:4455/error + verification: + enabled: true + use: code + ui_url: http://localhost:4455/verification + recovery: + enabled: true + use: code + ui_url: http://localhost:4455/recovery + methods: + password: + enabled: false + code: + passwordless_enabled: true + enabled: true + config: + lifespan: 1h + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/code/identity.traits.schema.json diff --git a/test/e2e/profiles/code/identity.code.only.traits.schema.json b/test/e2e/profiles/code/identity.code.only.traits.schema.json new file mode 100644 index 000000000000..97573e085d8c --- /dev/null +++ b/test/e2e/profiles/code/identity.code.only.traits.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + }, + "required": ["email"] + } + } +} diff --git a/test/e2e/profiles/code/identity.complex.traits.schema.json b/test/e2e/profiles/code/identity.complex.traits.schema.json new file mode 100644 index 000000000000..9eda33789066 --- /dev/null +++ b/test/e2e/profiles/code/identity.complex.traits.schema.json @@ -0,0 +1,77 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "title": "Your Username", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "email2": { + "type": "string", + "format": "email", + "title": "Your Second E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ] + } + } +} diff --git a/test/e2e/profiles/code/identity.traits.schema.json b/test/e2e/profiles/code/identity.traits.schema.json new file mode 100644 index 000000000000..268ae57da51a --- /dev/null +++ b/test/e2e/profiles/code/identity.traits.schema.json @@ -0,0 +1,39 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ] + } + } +} diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 6119a2b61f8b..3ea3e5bcab8e 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -136,8 +136,8 @@ prepare() { nc -zv localhost 4445 && exit 1 nc -zv localhost 4446 && exit 1 nc -zv localhost 4455 && exit 1 - nc -zv localhost 4456 && exit 1 nc -zv localhost 19006 && exit 1 + nc -zv localhost 4456 && exit 1 nc -zv localhost 4458 && exit 1 nc -zv localhost 4744 && exit 1 nc -zv localhost 4745 && exit 1 @@ -273,7 +273,7 @@ run() { nc -zv localhost 4433 && exit 1 ls -la . - for profile in email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do + for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do yq ea '. as $item ireduce ({}; . * $item )' test/e2e/profiles/kratos.base.yml "test/e2e/profiles/${profile}/.kratos.yml" > test/e2e/kratos.${profile}.yml cat "test/e2e/kratos.${profile}.yml" | envsubst | sponge "test/e2e/kratos.${profile}.yml" done diff --git a/text/id.go b/text/id.go index 0716ad991d8d..c0274db092ef 100644 --- a/text/id.go +++ b/text/id.go @@ -23,6 +23,8 @@ const ( InfoSelfServiceLoginContinueWebAuthn // 1010011 InfoSelfServiceLoginWebAuthnPasswordless // 1010012 InfoSelfServiceLoginContinue // 1010013 + InfoSelfServiceLoginEmailWithCodeSent // 1010014 + InfoSelfServiceLoginCode // 1010015 ) const ( @@ -34,11 +36,13 @@ const ( ) const ( - InfoSelfServiceRegistrationRoot ID = 1040000 + iota // 1040000 - InfoSelfServiceRegistration // 1040001 - InfoSelfServiceRegistrationWith // 1040002 - InfoSelfServiceRegistrationContinue // 1040003 - InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 + InfoSelfServiceRegistrationRoot ID = 1040000 + iota // 1040000 + InfoSelfServiceRegistration // 1040001 + InfoSelfServiceRegistrationWith // 1040002 + InfoSelfServiceRegistrationContinue // 1040003 + InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 + InfoSelfServiceRegistrationEmailWithCodeSent // 1040005 + InfoSelfServiceRegistrationRegisterCode // 1040006 ) const ( @@ -83,6 +87,8 @@ const ( InfoNodeLabelContinue // 1070009 InfoNodeLabelRecoveryCode // 1070010 InfoNodeLabelVerificationCode // 1070011 + InfoNodeLabelRegistrationCode // 1070012 + InfoNodeLabelLoginCode // 1070013 ) const ( @@ -128,21 +134,27 @@ const ( ErrorValidationPasswordMinLength ErrorValidationPasswordMaxLength ErrorValidationPasswordTooManyBreaches + ErrorValidationNoCodeUser + ErrorValidationTraitsMismatch ) const ( - ErrorValidationLogin ID = 4010000 + iota // 4010000 - ErrorValidationLoginFlowExpired // 4010001 - ErrorValidationLoginNoStrategyFound // 4010002 - ErrorValidationRegistrationNoStrategyFound // 4010003 - ErrorValidationSettingsNoStrategyFound // 4010004 - ErrorValidationRecoveryNoStrategyFound // 4010005 - ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLogin ID = 4010000 + iota // 4010000 + ErrorValidationLoginFlowExpired // 4010001 + ErrorValidationLoginNoStrategyFound // 4010002 + ErrorValidationRegistrationNoStrategyFound // 4010003 + ErrorValidationSettingsNoStrategyFound // 4010004 + ErrorValidationRecoveryNoStrategyFound // 4010005 + ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLoginRetrySuccess // 4010007 + ErrorValidationLoginCodeInvalidOrAlreadyUsed // 4010008 ) const ( - ErrorValidationRegistration ID = 4040000 + iota - ErrorValidationRegistrationFlowExpired + ErrorValidationRegistration ID = 4040000 + iota + ErrorValidationRegistrationFlowExpired // 4040001 + ErrorValidateionRegistrationRetrySuccess // 4040002 + ErrorValidationRegistrationCodeInvalidOrAlreadyUsed // 4040003 ) const ( diff --git a/text/message_login.go b/text/message_login.go index 8f69fa1989a8..d5f5243be71e 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -183,3 +183,38 @@ func NewInfoSelfServiceLoginContinue() *Message { Type: Info, } } + +func NewLoginEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceLoginEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the login.", + Context: context(nil), + } +} + +func NewErrorValidationLoginCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationLoginCodeInvalidOrAlreadyUsed, + Text: "The login code is invalid or has already been used. Please try again.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationLoginRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidationLoginRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + Context: context(nil), + } +} + +func NewInfoSelfServiceLoginCode() *Message { + return &Message{ + ID: InfoSelfServiceLoginCode, + Type: Info, + Text: "Sign in with code", + } +} diff --git a/text/message_node.go b/text/message_node.go index f3712ea75b6d..d9f3a03a0009 100644 --- a/text/message_node.go +++ b/text/message_node.go @@ -27,6 +27,22 @@ func NewInfoNodeLabelRecoveryCode() *Message { } } +func NewInfoNodeLabelRegistrationCode() *Message { + return &Message{ + ID: InfoNodeLabelRegistrationCode, + Text: "Registration code", + Type: Info, + } +} + +func NewInfoNodeLabelLoginCode() *Message { + return &Message{ + ID: InfoNodeLabelLoginCode, + Text: "Login code", + Type: Info, + } +} + func NewInfoNodeInputPassword() *Message { return &Message{ ID: InfoNodeLabelInputPassword, diff --git a/text/message_registration.go b/text/message_registration.go index 96fd9d4326b5..06ee4b08df94 100644 --- a/text/message_registration.go +++ b/text/message_registration.go @@ -54,3 +54,38 @@ func NewInfoSelfServiceRegistrationRegisterWebAuthn() *Message { Type: Info, } } + +func NewRegistrationEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the registration.", + Context: context(nil), + } +} + +func NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationRegistrationCodeInvalidOrAlreadyUsed, + Text: "The registration code is invalid or has already been used. Please try again.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationRegistrationRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidateionRegistrationRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + Context: context(nil), + } +} + +func NewInfoSelfServiceRegistrationRegisterCode() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterCode, + Text: "Sign up with code", + Type: Info, + } +} diff --git a/text/message_validation.go b/text/message_validation.go index 3467261bb68d..8fb2391bfdce 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -366,3 +366,21 @@ func NewErrorValidationSuchNoWebAuthnUser() *Message { Context: context(nil), } } + +func NewErrorValidationNoCodeUser() *Message { + return &Message{ + ID: ErrorValidationNoCodeUser, + Text: "This account does not exist or has not setup sign in with code.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationTraitsMismatch() *Message { + return &Message{ + ID: ErrorValidationTraitsMismatch, + Text: "The provided traits do not match the traits previously associated with this flow.", + Type: Error, + Context: context(nil), + } +} diff --git a/ui/container/container.go b/ui/container/container.go index 8c1b4a0b7a47..d80683cd2800 100644 --- a/ui/container/container.go +++ b/ui/container/container.go @@ -190,7 +190,7 @@ func (c *Container) ParseError(group node.UiNodeGroup, err error) error { default: // The pointer can be ignored because if there is an error, we'll just use // the empty field (global error). - var causes = e.Causes + causes := e.Causes if len(e.Causes) == 0 { pointer, _ := jsonschemax.JSONPointerToDotNotation(e.InstancePtr) c.AddMessage(group, translateValidationError(e), pointer) @@ -339,6 +339,7 @@ func (c *Container) AddMessage(group node.UiNodeGroup, err *text.Message, setFor func (c *Container) Scan(value interface{}) error { return sqlxx.JSONScan(c, value) } + func (c *Container) Value() (driver.Value, error) { return sqlxx.JSONValue(c) } diff --git a/x/xsql/sql.go b/x/xsql/sql.go index f2354d082ca1..7ee2591bcd74 100644 --- a/x/xsql/sql.go +++ b/x/xsql/sql.go @@ -28,6 +28,8 @@ import ( func CleanSQL(t testing.TB, c *pop.Connection) { ctx := context.Background() for _, table := range []string{ + new(code.LoginCode).TableName(ctx), + new(code.RegistrationCode).TableName(ctx), new(continuity.Container).TableName(ctx), new(courier.MessageDispatch).TableName(), new(courier.Message).TableName(ctx),