diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index a966cf27401e..ffcfca3169ec 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -108,7 +108,7 @@ path: /components/schemas/verificationFlowState/enum value: - choose_method - - sent_email + - sent - passed_challenge # End diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go index ef3027a936a3..e4e865a280f5 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -171,6 +171,11 @@ func init() { "NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(), "NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(), "NewInfoSelfServiceLoginCodeMFAHint": text.NewInfoSelfServiceLoginCodeMFAHint("{maskedIdentifier}"), + "NewErrorValidationInvalidCode": text.NewErrorValidationInvalidCode(), + "NewErrorCodeSent": text.NewErrorCodeSent(), + "NewErrorValidationSMSSpam": text.NewErrorValidationSMSSpam(), + "NewInfoNodeInputPhone": text.NewInfoNodeInputPhone(), + "NewInfoSelfServicePhoneVerificationSuccessful": text.NewInfoSelfServicePhoneVerificationSuccessful(), } } diff --git a/courier/template/courier/builtin/templates/login/sms.body.gotmpl b/courier/template/courier/builtin/templates/login/sms.body.gotmpl new file mode 100644 index 000000000000..6fb6cddf32b6 --- /dev/null +++ b/courier/template/courier/builtin/templates/login/sms.body.gotmpl @@ -0,0 +1 @@ +code {{ .Code }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/sms.body.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/sms.body.gotmpl new file mode 100644 index 000000000000..b9f513d0886f --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/sms.body.gotmpl @@ -0,0 +1 @@ +Your registration code is: {{ .RegistrationCode }} diff --git a/courier/template/courier/builtin/templates/test_stub/sms.body.gotmpl b/courier/template/courier/builtin/templates/test_stub/sms.body.gotmpl new file mode 100644 index 000000000000..a37e4640152d --- /dev/null +++ b/courier/template/courier/builtin/templates/test_stub/sms.body.gotmpl @@ -0,0 +1 @@ +stub sms body {{ .Body }} diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go index 1b3b55d4807c..4f857a3bd2ec 100644 --- a/courier/template/email/login_code_valid.go +++ b/courier/template/email/login_code_valid.go @@ -18,9 +18,10 @@ type ( model *LoginCodeValidModel } LoginCodeValidModel struct { - To string - LoginCode string - Identity map[string]interface{} + To string + LoginCode string + Identity map[string]interface{} + TransientPayload json.RawMessage } ) diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go index f984ffaeb6c6..2cbfa3f3e4bc 100644 --- a/courier/template/email/registration_code_valid.go +++ b/courier/template/email/registration_code_valid.go @@ -21,6 +21,7 @@ type ( To string Traits map[string]interface{} RegistrationCode string + TransientPayload json.RawMessage } ) diff --git a/courier/template/email/verification_code_valid.go b/courier/template/email/verification_code_valid.go index e23f48f70b5a..d0cce95b2a28 100644 --- a/courier/template/email/verification_code_valid.go +++ b/courier/template/email/verification_code_valid.go @@ -22,6 +22,7 @@ type ( VerificationURL string VerificationCode string Identity map[string]interface{} + TransientPayload json.RawMessage } ) diff --git a/courier/template/sms/login_code_valid.go b/courier/template/sms/login_code_valid.go index 194e113e6c6b..9727efddda44 100644 --- a/courier/template/sms/login_code_valid.go +++ b/courier/template/sms/login_code_valid.go @@ -17,9 +17,10 @@ type ( model *LoginCodeValidModel } LoginCodeValidModel struct { - To string - LoginCode string - Identity map[string]interface{} + To string + LoginCode string + Identity map[string]interface{} + TransientPayload json.RawMessage } ) diff --git a/courier/template/sms/registration_code_valid.go b/courier/template/sms/registration_code_valid.go new file mode 100644 index 000000000000..004ba98c28e5 --- /dev/null +++ b/courier/template/sms/registration_code_valid.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + RegistrationCodeValid struct { + deps template.Dependencies + model *RegistrationCodeValidModel + } + RegistrationCodeValidModel struct { + To string + RegistrationCode string + Traits map[string]interface{} + TransientPayload json.RawMessage + } +) + +func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid { + return &RegistrationCodeValid{deps: d, model: m} +} + +func (t *RegistrationCodeValid) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *RegistrationCodeValid) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "registration_code/valid/sms.body.gotmpl", + "registration_code/valid/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesLoginCodeValid(ctx).Body.PlainText, + ) +} + +func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *RegistrationCodeValid) TemplateType() template.TemplateType { + return template.TypeRegistrationCodeValid +} diff --git a/courier/template/sms/verification_code.go b/courier/template/sms/verification_code.go index f4ab6fc23359..b5fd210679a7 100644 --- a/courier/template/sms/verification_code.go +++ b/courier/template/sms/verification_code.go @@ -19,8 +19,10 @@ type ( VerificationCodeValidModel struct { To string + VerificationURL string VerificationCode string Identity map[string]interface{} + TransientPayload json.RawMessage } ) diff --git a/driver/config/config.go b/driver/config/config.go index 6af7e7c17c5c..81cff9938f51 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -195,6 +195,11 @@ const ( ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls" ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level" ViperKeyVersion = "version" + CodeTestNumbers = "selfservice.methods.code.config.test_numbers" + CodeSMSSpamProtectionEnabled = "selfservice.methods.code.config.sms_spam_protection.enabled" + CodeSMSSpamProtectionMaxSingleNumber = "selfservice.methods.code.config.sms_spam_protection.max_single_number" + CodeSMSSpamProtectionMaxNumbersRange = "selfservice.methods.code.config.sms_spam_protection.max_numbers_range" + ViperKeyCourierTemplatesLoginValidSMS = "courier.templates.login.valid.sms" ) const ( @@ -310,6 +315,7 @@ type ( CourierWorkerPullCount(ctx context.Context) int CourierWorkerPullWait(ctx context.Context) time.Duration CourierChannels(context.Context) ([]*CourierChannel, error) + CourierTemplatesLoginValidSMS(ctx context.Context) string } ) @@ -1528,6 +1534,26 @@ func (p *Config) getTLSCertificates(ctx context.Context, daemon, certBase64, key return nil } +func (p *Config) SelfServiceCodeTestNumbers(ctx context.Context) []string { + return p.GetProvider(ctx).Strings(CodeTestNumbers) +} + +func (p *Config) SelfServiceCodeSMSSpamProtectionEnabled() bool { + return p.p.Bool(CodeSMSSpamProtectionEnabled) +} + +func (p *Config) SelfServiceCodeSMSSpamProtectionMaxSingleNumber() int { + return p.p.Int(CodeSMSSpamProtectionMaxSingleNumber) +} + +func (p *Config) SelfServiceCodeSMSSpamProtectionMaxNumbersRange() int { + return p.p.Int(CodeSMSSpamProtectionMaxNumbersRange) +} + +func (p *Config) CourierTemplatesLoginValidSMS(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeyCourierTemplatesLoginValidSMS) +} + func (p *Config) GetProvider(ctx context.Context) *configx.Provider { return p.c.Config(ctx, p.p) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index e223b4cc1c79..d9a3e141f7e3 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1464,6 +1464,36 @@ "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", "examples": ["1h", "1m", "1s"] + }, + "test_numbers": { + "type": "array", + "description": "Phone numbers for test accounts", + "items": { + "type": "string" + } + }, + "sms_spam_protection": { + "title": "SMS spam protection", + "description": "Blocks from sending too many messages to the same number or to a range of numbers", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables spam protection", + "default": false + }, + "max_single_number": { + "type": "integer", + "title": "How many messages are allowed to be sent to a number per week", + "default": 50 + }, + "max_numbers_range": { + "type": "integer", + "title": "How many messages are allowed to be sent to a numbers range per week", + "default": 100 + } + } } } } diff --git a/identity/address.go b/identity/address.go index 2e9175642e84..2bb619761cb3 100644 --- a/identity/address.go +++ b/identity/address.go @@ -5,4 +5,5 @@ package identity const ( AddressTypeEmail = "email" + AddressTypePhone = "sms" ) diff --git a/identity/extension_credentials.go b/identity/extension_credentials.go index 3baa826b2e9c..d8b661c8631e 100644 --- a/identity/extension_credentials.go +++ b/identity/extension_credentials.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "github.com/nyaruka/phonenumbers" + "github.com/ory/jsonschema/v3" "github.com/ory/x/sqlxx" "github.com/ory/x/stringslice" @@ -23,7 +25,7 @@ type SchemaExtensionCredentials struct { } func NewSchemaExtensionCredentials(i *Identity) *SchemaExtensionCredentials { - return &SchemaExtensionCredentials{i: i} + return &SchemaExtensionCredentials{i: i, v: make(map[CredentialsType][]string)} } func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}) { @@ -64,12 +66,14 @@ func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s sch } r.setIdentifier(CredentialsTypeCodeAuth, value) - // 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, CredentialsIdentifierAddressTypePhone) + case f.AddCase(AddressTypePhone): + phoneNumber, err := phonenumbers.Parse(fmt.Sprintf("%s", value), "") + if err != nil { + validationError := ctx.Error("format", "%s", err) + return validationError + } + e164 := fmt.Sprintf("+%d%d", *phoneNumber.CountryCode, *phoneNumber.NationalNumber) + r.setIdentifier(CredentialsTypeCodeAuth, e164) default: return ctx.Error("", "credentials.code.via has unknown value %q", s.Credentials.Code.Via) } @@ -79,5 +83,30 @@ func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s sch } func (r *SchemaExtensionCredentials) Finish() error { + r.l.Lock() + defer r.l.Unlock() + + for ct := range r.i.Credentials { + _, ok := r.v[ct] + if !ok { + r.v[ct] = []string{} + } + } + for ct, identifiers := range r.v { + cred, ok := r.i.GetCredentials(ct) + if !ok { + cred = &Credentials{ + Type: ct, + Identifiers: []string{}, + Config: sqlxx.JSONRawMessage{}, + } + } + + if ct == CredentialsTypePassword || ct == CredentialsTypeCodeAuth { + cred.Identifiers = identifiers + r.i.SetCredentials(ct, *cred) + } + } + return nil } diff --git a/identity/extension_credentials_test.go b/identity/extension_credentials_test.go index 95cd9d000c6a..bd2d73d64422 100644 --- a/identity/extension_credentials_test.go +++ b/identity/extension_credentials_test.go @@ -6,6 +6,7 @@ package identity_test import ( "bytes" "context" + "errors" "fmt" "testing" @@ -36,6 +37,15 @@ func TestSchemaExtensionCredentials(t *testing.T) { expect: []string{"foo@ory.sh"}, ct: identity.CredentialsTypePassword, }, + { + doc: `{}`, + schema: "file://./stub/extension/credentials/schema.json", + expect: []string{}, + existing: &identity.Credentials{ + Identifiers: []string{"foo@ory.sh"}, + }, + ct: identity.CredentialsTypePassword, + }, { doc: `{"emails":["foo@ory.sh","foo@ory.sh","bar@ory.sh"], "username": "foobar"}`, schema: "file://./stub/extension/credentials/multi.schema.json", @@ -87,6 +97,18 @@ func TestSchemaExtensionCredentials(t *testing.T) { }, ct: identity.CredentialsTypeCodeAuth, }, + { + doc: `{"phone":"not-valid-number"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + ct: identity.CredentialsTypeCodeAuth, + expectErr: errors.New("I[#/phone] S[#/properties/phone] validation failed\n I[#/phone] S[#/properties/phone/format] \"not-valid-number\" is not valid \"tel\"\n I[#/phone] S[#/properties/phone/format] the phone number supplied is not a number"), + }, + { + doc: `{"phone":"+4407376494399"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expect: []string{"+447376494399"}, + ct: identity.CredentialsTypeCodeAuth, + }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { c := jsonschema.NewCompiler() @@ -103,12 +125,16 @@ func TestSchemaExtensionCredentials(t *testing.T) { err = c.MustCompile(ctx, tc.schema).Validate(bytes.NewBufferString(tc.doc)) if tc.expectErr != nil { require.EqualError(t, err, tc.expectErr.Error()) + } else { + require.NoError(t, err) } require.NoError(t, e.Finish()) - credentials, ok := i.GetCredentials(tc.ct) - require.True(t, ok) - assert.ElementsMatch(t, tc.expect, credentials.Identifiers) + if tc.expectErr == nil { + credentials, ok := i.GetCredentials(tc.ct) + require.True(t, ok) + assert.ElementsMatch(t, tc.expect, credentials.Identifiers) + } }) } } diff --git a/identity/extension_verification.go b/identity/extension_verification.go index 3b3f92581c37..03726dd8f762 100644 --- a/identity/extension_verification.go +++ b/identity/extension_verification.go @@ -22,14 +22,15 @@ func init() { } type SchemaExtensionVerification struct { - lifespan time.Duration - l sync.Mutex - v []VerifiableAddress - i *Identity + lifespan time.Duration + codeTestNumbers []string + l sync.Mutex + v []VerifiableAddress + i *Identity } -func NewSchemaExtensionVerification(i *Identity, lifespan time.Duration) *SchemaExtensionVerification { - return &SchemaExtensionVerification{i: i, lifespan: lifespan} +func NewSchemaExtensionVerification(i *Identity, lifespan time.Duration, codeTestNumbers []string) *SchemaExtensionVerification { + return &SchemaExtensionVerification{i: i, lifespan: lifespan, codeTestNumbers: codeTestNumbers} } const ( @@ -122,3 +123,20 @@ func has(haystack []VerifiableAddress, needle *VerifiableAddress) *VerifiableAdd } return nil } + +func (r *SchemaExtensionVerification) checkTelFormat(ctx jsonschema.ValidationContext, value interface{}) error { + validationError := ctx.Error("format", "%q is not valid %q", value, "phone") + num, ok := value.(string) + if !ok { + return validationError + } + for _, n := range r.codeTestNumbers { + if num == n { + return nil + } + } + if !jsonschema.Formats["tel"](num) { + return validationError + } + return nil +} diff --git a/identity/extension_verification_test.go b/identity/extension_verification_test.go index ebf2c09f207e..b69b0aa98d89 100644 --- a/identity/extension_verification_test.go +++ b/identity/extension_verification_test.go @@ -378,7 +378,7 @@ func TestSchemaExtensionVerification(t *testing.T) { runner, err := schema.NewExtensionRunner(ctx) require.NoError(t, err) - e := NewSchemaExtensionVerification(id, time.Minute) + e := NewSchemaExtensionVerification(id, time.Minute, []string{}) runner.AddRunner(e).Register(c) err = c.MustCompile(ctx, tc.schema).Validate(bytes.NewBufferString(tc.doc)) diff --git a/identity/identity_verification.go b/identity/identity_verification.go index 54ac435ec9fd..2e1ffb90df21 100644 --- a/identity/identity_verification.go +++ b/identity/identity_verification.go @@ -15,6 +15,7 @@ import ( const ( VerifiableAddressTypeEmail VerifiableAddressType = AddressTypeEmail + VerifiableAddressTypePhone VerifiableAddressType = AddressTypePhone VerifiableAddressStatusPending VerifiableAddressStatus = "pending" VerifiableAddressStatusSent VerifiableAddressStatus = "sent" diff --git a/identity/manager_test.go b/identity/manager_test.go index 81001c9ba9c6..46cfad36b921 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -30,11 +30,17 @@ import ( func TestManager(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/manager.schema.json") + twoCredentiaTypesScheemaID := "two-credential-types" + testhelpers.SetIdentitySchemas(t, conf, map[string]string{ + "default": "file://./stub/manager.schema.json", + twoCredentiaTypesScheemaID: "file://./stub/manager-two-credential-types.schema.json", + }) + conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default") extensionSchemaID := testhelpers.UseIdentitySchema(t, conf, "file://./stub/extension.schema.json") conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf.MustSet(ctx, config.CodeTestNumbers, []string{"+12223333333"}) t.Run("case=should fail to create because validation fails", func(t *testing.T) { i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) @@ -42,6 +48,17 @@ func TestManager(t *testing.T) { require.Error(t, reg.IdentityManager().Create(context.Background(), i)) }) + t.Run("case=should create correct identifiers for two credential types", func(t *testing.T) { + i := identity.NewIdentity(twoCredentiaTypesScheemaID) + i.Traits = identity.Traits(`{"email": "some@email.domain", "phone": "+12223333333"}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + + assert.Equal(t, 1, len(i.Credentials["password"].Identifiers)) + assert.Equal(t, "some@email.domain", i.Credentials["password"].Identifiers[0]) + assert.Equal(t, 1, len(i.Credentials["code"].Identifiers)) + assert.Equal(t, "+12223333333", i.Credentials["code"].Identifiers[0]) + }) + newTraits := func(email string, unprotected string) identity.Traits { return identity.Traits(fmt.Sprintf(`{"email":"%[1]s","email_verify":"%[1]s","email_recovery":"%[1]s","email_creds":"%[1]s","unprotected": "%[2]s"}`, email, unprotected)) } diff --git a/identity/stub/extension/credentials/code.schema.json b/identity/stub/extension/credentials/code.schema.json index bef244bc9ae5..3741ab0c853f 100644 --- a/identity/stub/extension/credentials/code.schema.json +++ b/identity/stub/extension/credentials/code.schema.json @@ -15,6 +15,18 @@ } } } + }, + "phone": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } } } } diff --git a/identity/stub/manager-two-credential-types.schema.json b/identity/stub/manager-two-credential-types.schema.json new file mode 100644 index 000000000000..d3ae9574e3ae --- /dev/null +++ b/identity/stub/manager-two-credential-types.schema.json @@ -0,0 +1,43 @@ +{ + "$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", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "phone": { + "type": "string", + "format": "phone", + "title": "Your phone number", + "minLength": 11, + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/identity/validator.go b/identity/validator.go index 3bd8f9476fa5..6f226f4a77f6 100644 --- a/identity/validator.go +++ b/identity/validator.go @@ -67,7 +67,11 @@ func (v *Validator) Validate(ctx context.Context, i *Identity) error { return otelx.WithSpan(ctx, "identity.Validator.Validate", func(ctx context.Context) error { return v.ValidateWithRunner(ctx, i, NewSchemaExtensionCredentials(i), - NewSchemaExtensionVerification(i, v.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx)), + NewSchemaExtensionVerification( + i, + v.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), + v.d.Config().SelfServiceCodeTestNumbers(ctx), + ), NewSchemaExtensionRecovery(i), ) }) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_submit_self_service_login_flow_with_code_method.go b/internal/client-go/model_submit_self_service_login_flow_with_code_method.go new file mode 100644 index 000000000000..268a91ab637e --- /dev/null +++ b/internal/client-go/model_submit_self_service_login_flow_with_code_method.go @@ -0,0 +1,226 @@ +/* + * 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" +) + +// SubmitSelfServiceLoginFlowWithCodeMethod struct for SubmitSelfServiceLoginFlowWithCodeMethod +type SubmitSelfServiceLoginFlowWithCodeMethod struct { + // One-time code. + Code *string `json:"code,omitempty"` + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The user's phone number. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + Method *string `json:"method,omitempty"` +} + +// NewSubmitSelfServiceLoginFlowWithCodeMethod instantiates a new SubmitSelfServiceLoginFlowWithCodeMethod 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 NewSubmitSelfServiceLoginFlowWithCodeMethod() *SubmitSelfServiceLoginFlowWithCodeMethod { + this := SubmitSelfServiceLoginFlowWithCodeMethod{} + return &this +} + +// NewSubmitSelfServiceLoginFlowWithCodeMethodWithDefaults instantiates a new SubmitSelfServiceLoginFlowWithCodeMethod 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 NewSubmitSelfServiceLoginFlowWithCodeMethodWithDefaults() *SubmitSelfServiceLoginFlowWithCodeMethod { + this := SubmitSelfServiceLoginFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) GetMethod() string { + if o == nil || o.Method == nil { + var ret string + return ret + } + return *o.Method +} + +// GetMethodOk returns a tuple with the Method field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil || o.Method == nil { + return nil, false + } + return o.Method, true +} + +// HasMethod returns a boolean if a field has been set. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) HasMethod() bool { + if o != nil && o.Method != nil { + return true + } + + return false +} + +// SetMethod gets a reference to the given string and assigns it to the Method field. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = &v +} + +func (o SubmitSelfServiceLoginFlowWithCodeMethod) 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 o.Identifier != nil { + toSerialize["identifier"] = o.Identifier + } + if o.Method != nil { + toSerialize["method"] = o.Method + } + return json.Marshal(toSerialize) +} + +type NullableSubmitSelfServiceLoginFlowWithCodeMethod struct { + value *SubmitSelfServiceLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableSubmitSelfServiceLoginFlowWithCodeMethod) Get() *SubmitSelfServiceLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableSubmitSelfServiceLoginFlowWithCodeMethod) Set(val *SubmitSelfServiceLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableSubmitSelfServiceLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableSubmitSelfServiceLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSubmitSelfServiceLoginFlowWithCodeMethod(val *SubmitSelfServiceLoginFlowWithCodeMethod) *NullableSubmitSelfServiceLoginFlowWithCodeMethod { + return &NullableSubmitSelfServiceLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableSubmitSelfServiceLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSubmitSelfServiceLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_submit_self_service_registration_flow_with_code_method_body.go b/internal/client-go/model_submit_self_service_registration_flow_with_code_method_body.go new file mode 100644 index 000000000000..a2e9bf8562e6 --- /dev/null +++ b/internal/client-go/model_submit_self_service_registration_flow_with_code_method_body.go @@ -0,0 +1,212 @@ +/* + * 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" +) + +// SubmitSelfServiceRegistrationFlowWithCodeMethodBody SubmitSelfServiceRegistrationFlowWithCodeMethodBody is used to decode the registration form payload when using the code method. +type SubmitSelfServiceRegistrationFlowWithCodeMethodBody struct { + // Code from the code + 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"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` +} + +// NewSubmitSelfServiceRegistrationFlowWithCodeMethodBody instantiates a new SubmitSelfServiceRegistrationFlowWithCodeMethodBody 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 NewSubmitSelfServiceRegistrationFlowWithCodeMethodBody(method string, traits map[string]interface{}) *SubmitSelfServiceRegistrationFlowWithCodeMethodBody { + this := SubmitSelfServiceRegistrationFlowWithCodeMethodBody{} + this.Method = method + this.Traits = traits + return &this +} + +// NewSubmitSelfServiceRegistrationFlowWithCodeMethodBodyWithDefaults instantiates a new SubmitSelfServiceRegistrationFlowWithCodeMethodBody 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 NewSubmitSelfServiceRegistrationFlowWithCodeMethodBodyWithDefaults() *SubmitSelfServiceRegistrationFlowWithCodeMethodBody { + this := SubmitSelfServiceRegistrationFlowWithCodeMethodBody{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +func (o SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 true { + toSerialize["traits"] = o.Traits + } + return json.Marshal(toSerialize) +} + +type NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody struct { + value *SubmitSelfServiceRegistrationFlowWithCodeMethodBody + isSet bool +} + +func (v NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) Get() *SubmitSelfServiceRegistrationFlowWithCodeMethodBody { + return v.value +} + +func (v *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) Set(val *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) { + v.value = val + v.isSet = true +} + +func (v NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) IsSet() bool { + return v.isSet +} + +func (v *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody(val *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody { + return &NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody{value: val, isSet: true} +} + +func (v NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_verification_flow_state.go b/internal/client-go/model_verification_flow_state.go index bea74568c94d..295d1a65e341 100644 --- a/internal/client-go/model_verification_flow_state.go +++ b/internal/client-go/model_verification_flow_state.go @@ -22,7 +22,7 @@ type VerificationFlowState string // List of verificationFlowState const ( VERIFICATIONFLOWSTATE_CHOOSE_METHOD VerificationFlowState = "choose_method" - VERIFICATIONFLOWSTATE_SENT_EMAIL VerificationFlowState = "sent_email" + VERIFICATIONFLOWSTATE_SENT VerificationFlowState = "sent" VERIFICATIONFLOWSTATE_PASSED_CHALLENGE VerificationFlowState = "passed_challenge" ) @@ -33,7 +33,7 @@ func (v *VerificationFlowState) UnmarshalJSON(src []byte) error { return err } enumTypeValue := VerificationFlowState(value) - for _, existing := range []VerificationFlowState{"choose_method", "sent_email", "passed_challenge"} { + for _, existing := range []VerificationFlowState{"choose_method", "sent", "passed_challenge"} { if existing == enumTypeValue { *v = enumTypeValue return nil diff --git a/internal/httpclient/model_submit_self_service_login_flow_with_code_method.go b/internal/httpclient/model_submit_self_service_login_flow_with_code_method.go new file mode 100644 index 000000000000..268a91ab637e --- /dev/null +++ b/internal/httpclient/model_submit_self_service_login_flow_with_code_method.go @@ -0,0 +1,226 @@ +/* + * 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" +) + +// SubmitSelfServiceLoginFlowWithCodeMethod struct for SubmitSelfServiceLoginFlowWithCodeMethod +type SubmitSelfServiceLoginFlowWithCodeMethod struct { + // One-time code. + Code *string `json:"code,omitempty"` + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // The user's phone number. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + Method *string `json:"method,omitempty"` +} + +// NewSubmitSelfServiceLoginFlowWithCodeMethod instantiates a new SubmitSelfServiceLoginFlowWithCodeMethod 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 NewSubmitSelfServiceLoginFlowWithCodeMethod() *SubmitSelfServiceLoginFlowWithCodeMethod { + this := SubmitSelfServiceLoginFlowWithCodeMethod{} + return &this +} + +// NewSubmitSelfServiceLoginFlowWithCodeMethodWithDefaults instantiates a new SubmitSelfServiceLoginFlowWithCodeMethod 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 NewSubmitSelfServiceLoginFlowWithCodeMethodWithDefaults() *SubmitSelfServiceLoginFlowWithCodeMethod { + this := SubmitSelfServiceLoginFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) 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 *SubmitSelfServiceLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value if set, zero value otherwise. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) GetMethod() string { + if o == nil || o.Method == nil { + var ret string + return ret + } + return *o.Method +} + +// GetMethodOk returns a tuple with the Method field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil || o.Method == nil { + return nil, false + } + return o.Method, true +} + +// HasMethod returns a boolean if a field has been set. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) HasMethod() bool { + if o != nil && o.Method != nil { + return true + } + + return false +} + +// SetMethod gets a reference to the given string and assigns it to the Method field. +func (o *SubmitSelfServiceLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = &v +} + +func (o SubmitSelfServiceLoginFlowWithCodeMethod) 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 o.Identifier != nil { + toSerialize["identifier"] = o.Identifier + } + if o.Method != nil { + toSerialize["method"] = o.Method + } + return json.Marshal(toSerialize) +} + +type NullableSubmitSelfServiceLoginFlowWithCodeMethod struct { + value *SubmitSelfServiceLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableSubmitSelfServiceLoginFlowWithCodeMethod) Get() *SubmitSelfServiceLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableSubmitSelfServiceLoginFlowWithCodeMethod) Set(val *SubmitSelfServiceLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableSubmitSelfServiceLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableSubmitSelfServiceLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSubmitSelfServiceLoginFlowWithCodeMethod(val *SubmitSelfServiceLoginFlowWithCodeMethod) *NullableSubmitSelfServiceLoginFlowWithCodeMethod { + return &NullableSubmitSelfServiceLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableSubmitSelfServiceLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSubmitSelfServiceLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_submit_self_service_registration_flow_with_code_method_body.go b/internal/httpclient/model_submit_self_service_registration_flow_with_code_method_body.go new file mode 100644 index 000000000000..a2e9bf8562e6 --- /dev/null +++ b/internal/httpclient/model_submit_self_service_registration_flow_with_code_method_body.go @@ -0,0 +1,212 @@ +/* + * 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" +) + +// SubmitSelfServiceRegistrationFlowWithCodeMethodBody SubmitSelfServiceRegistrationFlowWithCodeMethodBody is used to decode the registration form payload when using the code method. +type SubmitSelfServiceRegistrationFlowWithCodeMethodBody struct { + // Code from the code + 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"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` +} + +// NewSubmitSelfServiceRegistrationFlowWithCodeMethodBody instantiates a new SubmitSelfServiceRegistrationFlowWithCodeMethodBody 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 NewSubmitSelfServiceRegistrationFlowWithCodeMethodBody(method string, traits map[string]interface{}) *SubmitSelfServiceRegistrationFlowWithCodeMethodBody { + this := SubmitSelfServiceRegistrationFlowWithCodeMethodBody{} + this.Method = method + this.Traits = traits + return &this +} + +// NewSubmitSelfServiceRegistrationFlowWithCodeMethodBodyWithDefaults instantiates a new SubmitSelfServiceRegistrationFlowWithCodeMethodBody 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 NewSubmitSelfServiceRegistrationFlowWithCodeMethodBodyWithDefaults() *SubmitSelfServiceRegistrationFlowWithCodeMethodBody { + this := SubmitSelfServiceRegistrationFlowWithCodeMethodBody{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetMethod(v string) { + o.Method = v +} + +// GetTraits returns the Traits field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +func (o SubmitSelfServiceRegistrationFlowWithCodeMethodBody) 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 true { + toSerialize["traits"] = o.Traits + } + return json.Marshal(toSerialize) +} + +type NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody struct { + value *SubmitSelfServiceRegistrationFlowWithCodeMethodBody + isSet bool +} + +func (v NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) Get() *SubmitSelfServiceRegistrationFlowWithCodeMethodBody { + return v.value +} + +func (v *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) Set(val *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) { + v.value = val + v.isSet = true +} + +func (v NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) IsSet() bool { + return v.isSet +} + +func (v *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody(val *SubmitSelfServiceRegistrationFlowWithCodeMethodBody) *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody { + return &NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody{value: val, isSet: true} +} + +func (v NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSubmitSelfServiceRegistrationFlowWithCodeMethodBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/registrationhelpers/helpers.go b/internal/registrationhelpers/helpers.go index 67a636b567fb..cdfdfd5c74a7 100644 --- a/internal/registrationhelpers/helpers.go +++ b/internal/registrationhelpers/helpers.go @@ -9,6 +9,7 @@ import ( _ "embed" "encoding/json" "fmt" + "github.com/ory/kratos/identity" "net/http" "net/http/httptest" "net/url" @@ -278,6 +279,8 @@ func AssertRegistrationRespectsValidation(t *testing.T, reg *driver.RegistryDefa func AssertCommonErrorCases(t *testing.T, flows []string) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), + map[string]interface{}{"enabled": false}) testhelpers.SetDefaultIdentitySchemaFromRaw(conf, basicSchema) uiTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) diff --git a/internal/testhelpers/json.go b/internal/testhelpers/json.go index f9af044d811b..9590c1f60a1f 100644 --- a/internal/testhelpers/json.go +++ b/internal/testhelpers/json.go @@ -24,3 +24,10 @@ func LogJSON(t *testing.T, v interface{}) { require.NoError(t, err) t.Logf("\n%s\n---", out) } + +func PrettyJSON(t *testing.T, body []byte) string { + var out bytes.Buffer + require.NoError(t, json.Indent(&out, body, "", "\t")) + + return out.String() +} diff --git a/internal/testhelpers/selfservice_login.go b/internal/testhelpers/selfservice_login.go index 2bd20f81dfd4..9d3df9548da9 100644 --- a/internal/testhelpers/selfservice_login.go +++ b/internal/testhelpers/selfservice_login.go @@ -236,6 +236,28 @@ func SubmitLoginForm( } } + hc.Transport = NewTransportWithLogger(hc.Transport, t) + + var f = InitializeLoginFlow(t, isAPI, hc, publicTS, isSPA, forced) + + return SubmitLoginFormWithFlow(t, isAPI, hc, withValues, isSPA, expectedStatusCode, expectedURL, f) +} + +func InitializeLoginFlow( + t *testing.T, + isAPI bool, + hc *http.Client, + publicTS *httptest.Server, + isSPA bool, + forced bool, +) *kratos.LoginFlow { + if hc == nil { + hc = new(http.Client) + if !isAPI { + hc = NewClientWithCookies(t) + } + } + hc.Transport = NewTransportWithLogger(hc.Transport, t) var f *kratos.LoginFlow if isAPI { @@ -246,9 +268,31 @@ func SubmitLoginForm( time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. - payload := SDKFormFieldsToURLValues(f.Ui.Nodes) + return f +} + +func SubmitLoginFormWithFlow( + t *testing.T, + isAPI bool, + hc *http.Client, + withValues func(v url.Values), + isSPA bool, + expectedStatusCode int, + expectedURL string, + flow *kratos.LoginFlow, +) string { + if hc == nil { + hc = new(http.Client) + if !isAPI { + hc = NewClientWithCookies(t) + } + } + + hc.Transport = NewTransportWithLogger(hc.Transport, t) + + payload := SDKFormFieldsToURLValues(flow.Ui.Nodes) withValues(payload) - b, res := LoginMakeRequest(t, isAPI, isSPA, f, hc, EncodeFormAsJSON(t, isAPI, payload)) + b, res := LoginMakeRequest(t, isAPI, isSPA, flow, hc, EncodeFormAsJSON(t, isAPI, payload)) assert.EqualValues(t, expectedStatusCode, res.StatusCode, "%s", b) assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b) diff --git a/internal/testhelpers/selfservice_recovery.go b/internal/testhelpers/selfservice_recovery.go index c1ff66794553..200cf35882d2 100644 --- a/internal/testhelpers/selfservice_recovery.go +++ b/internal/testhelpers/selfservice_recovery.go @@ -114,12 +114,14 @@ func SubmitVerificationForm( withValues func(v url.Values), expectedStatusCode int, expectedURL string, + f *kratos.VerificationFlow, ) string { - var f *kratos.VerificationFlow - if isAPI { - f = InitializeVerificationFlowViaAPI(t, hc, publicTS) - } else { - f = InitializeVerificationFlowViaBrowser(t, hc, isSPA, publicTS) + if f == nil { + if isAPI { + f = InitializeVerificationFlowViaAPI(t, hc, publicTS) + } else { + f = InitializeVerificationFlowViaBrowser(t, hc, isSPA, publicTS) + } } time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. diff --git a/internal/testhelpers/selfservice_registration.go b/internal/testhelpers/selfservice_registration.go index 0cab8517e541..1a7f970a8ab8 100644 --- a/internal/testhelpers/selfservice_registration.go +++ b/internal/testhelpers/selfservice_registration.go @@ -130,18 +130,57 @@ func SubmitRegistrationForm( } hc.Transport = NewTransportWithLogger(hc.Transport, t) - var payload *kratos.RegistrationFlow + + var f = InitializeRegistrationFlow(t, isAPI, hc, publicTS, isSPA) + + return SubmitRegistrationFormWithFlow(t, isAPI, hc, withValues, isSPA, expectedStatusCode, expectedURL, f) +} + +func InitializeRegistrationFlow( + t *testing.T, + isAPI bool, + hc *http.Client, + publicTS *httptest.Server, + isSPA bool, +) *kratos.RegistrationFlow { + if hc == nil { + hc = new(http.Client) + } + + hc.Transport = NewTransportWithLogger(hc.Transport, t) + + var flow *kratos.RegistrationFlow if isAPI { - payload = InitializeRegistrationFlowViaAPI(t, hc, publicTS) + flow = InitializeRegistrationFlowViaAPI(t, hc, publicTS) } else { - payload = InitializeRegistrationFlowViaBrowser(t, hc, publicTS, isSPA, false, false) + flow = InitializeRegistrationFlowViaBrowser(t, hc, publicTS, isSPA, false, false) } time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. - values := SDKFormFieldsToURLValues(payload.Ui.Nodes) + return flow +} + +func SubmitRegistrationFormWithFlow( + t *testing.T, + isAPI bool, + hc *http.Client, + withValues func(v url.Values), + isSPA bool, + expectedStatusCode int, + expectedURL string, + flow *kratos.RegistrationFlow, +) string { + if hc == nil { + hc = new(http.Client) + } + + hc.Transport = NewTransportWithLogger(hc.Transport, t) + + values := SDKFormFieldsToURLValues(flow.Ui.Nodes) withValues(values) - b, res := RegistrationMakeRequest(t, isAPI, isSPA, payload, hc, EncodeFormAsJSON(t, isAPI, values)) + + b, res := RegistrationMakeRequest(t, isAPI, isSPA, flow, hc, EncodeFormAsJSON(t, isAPI, values)) assert.EqualValues(t, expectedStatusCode, res.StatusCode, assertx.PrettifyJSONPayload(t, b)) assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, b)) return b diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index 9a82e85ccbc7..e64a2d0c35f5 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -444,7 +444,7 @@ func TestFlowLifecycle(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), }, }, - Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), + Traits: identity.Traits(fmt.Sprintf(`{"username":"%s"}`, email)), SchemaID: config.DefaultIdentityTraitsSchemaID, } diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go index b3ef3d9054df..ede21c863981 100644 --- a/selfservice/flow/registration/error.go +++ b/selfservice/flow/registration/error.go @@ -97,8 +97,8 @@ func (s *ErrorHandler) WriteFlowError( } trace.SpanFromContext(r.Context()).AddEvent(events.NewRegistrationFailed(r.Context(), string(f.Type), f.Active.String())) - if expired, inner := s.PrepareReplacementForExpiredFlow(w, r, f, err); inner != nil { - s.forward(w, r, f, err) + if expired, innerErr := s.PrepareReplacementForExpiredFlow(w, r, f, err); innerErr != nil { + s.forward(w, r, f, innerErr) return } else if expired != nil { if f.Type == flow.TypeAPI || x.IsJSONRequest(r) { @@ -110,24 +110,24 @@ func (s *ErrorHandler) WriteFlowError( } f.UI.ResetMessages() - if err := f.UI.ParseError(group, err); err != nil { - s.forward(w, r, f, err) + if innerErr := f.UI.ParseError(group, err); innerErr != nil { + s.forward(w, r, f, innerErr) return } - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - s.forward(w, r, f, err) + ds, innerErr := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if innerErr != nil { + s.forward(w, r, f, innerErr) return } - if err := SortNodes(r.Context(), f.UI.Nodes, ds.String()); err != nil { - s.forward(w, r, f, err) + if innerErr := SortNodes(r.Context(), f.UI.Nodes, ds.String()); innerErr != nil { + s.forward(w, r, f, innerErr) return } - if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(r.Context(), f); err != nil { - s.forward(w, r, f, err) + if innerErr := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(r.Context(), f); innerErr != nil { + s.forward(w, r, f, innerErr) return } diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go index 76a0683fc19d..715dd3936983 100644 --- a/selfservice/flow/state.go +++ b/selfservice/flow/state.go @@ -28,12 +28,13 @@ type State string const ( StateChooseMethod State = "choose_method" StateEmailSent State = "sent_email" + StateSMSSent State = "sent_sms" StatePassedChallenge State = "passed_challenge" StateShowForm State = "show_form" StateSuccess State = "success" ) -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} +var states = []State{StateChooseMethod, StateEmailSent, StateSMSSent, StatePassedChallenge} func indexOf(current State) int { for k, s := range states { @@ -45,10 +46,17 @@ func indexOf(current State) int { } func HasReachedState(expected, actual State) bool { + if expected == StateEmailSent && actual == StateSMSSent || + expected == StateSMSSent && actual == StateEmailSent { + return true + } return indexOf(actual) >= indexOf(expected) } func NextState(current State) State { + if current == StateEmailSent { + return StatePassedChallenge + } if current == StatePassedChallenge { return StatePassedChallenge } diff --git a/selfservice/flow/verification/error.go b/selfservice/flow/verification/error.go index b39875b233d0..7a15a307900a 100644 --- a/selfservice/flow/verification/error.go +++ b/selfservice/flow/verification/error.go @@ -115,7 +115,9 @@ func (s *ErrorHandler) WriteFlowError( return } - f.Active = sqlxx.NullString(group) + if (f.Active != "link" && f.Active != "code") || (group != "link" && group != "code") { + f.Active = sqlxx.NullString(group) + } if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { s.forward(w, r, f, err) return diff --git a/selfservice/flow/verification/fake_strategy.go b/selfservice/flow/verification/fake_strategy.go index d497fb5111f3..70330f6e94a5 100644 --- a/selfservice/flow/verification/fake_strategy.go +++ b/selfservice/flow/verification/fake_strategy.go @@ -5,6 +5,7 @@ package verification import ( "context" + "encoding/json" "net/http" "github.com/ory/kratos/identity" @@ -31,7 +32,7 @@ func (f FakeStrategy) Verify(_ http.ResponseWriter, _ *http.Request, _ *Flow) (e return nil } -func (f FakeStrategy) SendVerificationEmail(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress) error { +func (f FakeStrategy) SendVerification(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress, json.RawMessage) error { return nil } diff --git a/selfservice/flow/verification/strategy.go b/selfservice/flow/verification/strategy.go index 3d270bfb8732..e55fc32d37c7 100644 --- a/selfservice/flow/verification/strategy.go +++ b/selfservice/flow/verification/strategy.go @@ -5,6 +5,7 @@ package verification import ( "context" + "encoding/json" "net/http" "github.com/ory/kratos/identity" @@ -29,7 +30,7 @@ type ( 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 + SendVerification(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress, json.RawMessage) error } AdminHandler interface { RegisterAdminVerificationRoutes(admin *x.RouterAdmin) diff --git a/selfservice/flowhelpers/login_test.go b/selfservice/flowhelpers/login_test.go index 3506c73c8513..1c0416059d9a 100644 --- a/selfservice/flowhelpers/login_test.go +++ b/selfservice/flowhelpers/login_test.go @@ -30,6 +30,7 @@ func TestGuessForcedLoginIdentifier(t *testing.T) { Identifiers: []string{"foobar"}, } i.Credentials[identity.CredentialsTypePassword] = ic + i.Traits = []byte(`{"email":"foobar"}`) require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) req := httptest.NewRequest("GET", "/sessions/whoami", nil) diff --git a/selfservice/flowhelpers/stub/login.schema.json b/selfservice/flowhelpers/stub/login.schema.json index 82f85811a16a..a15e45581c08 100644 --- a/selfservice/flowhelpers/stub/login.schema.json +++ b/selfservice/flowhelpers/stub/login.schema.json @@ -5,7 +5,19 @@ "type": "object", "properties": { "traits": { - "type": "object" + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } } } } diff --git a/selfservice/hook/.schema/verification.schema.json b/selfservice/hook/.schema/verification.schema.json new file mode 100644 index 000000000000..8dca1d248916 --- /dev/null +++ b/selfservice/hook/.schema/verification.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/verification.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/hook/schema.go b/selfservice/hook/schema.go new file mode 100644 index 000000000000..cbace14bab44 --- /dev/null +++ b/selfservice/hook/schema.go @@ -0,0 +1,6 @@ +package hook + +import _ "embed" + +//go:embed .schema/verification.schema.json +var verificationMethodSchema []byte diff --git a/selfservice/hook/stub/verify.phone.schema.json b/selfservice/hook/stub/verify.phone.schema.json new file mode 100644 index 000000000000..2009f7c0913b --- /dev/null +++ b/selfservice/hook/stub/verify.phone.schema.json @@ -0,0 +1,22 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } + } + } +} diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index c75cf1ce073b..efe74e603e6a 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -5,6 +5,8 @@ package hook import ( "context" + "encoding/json" + "github.com/ory/x/decoderx" "net/http" "github.com/gofrs/uuid" @@ -36,7 +38,11 @@ type ( x.WriterProvider } Verifier struct { - r verifierDependencies + r verifierDependencies + dx *decoderx.HTTP + } + transientPayloadBody struct { + TransientPayload json.RawMessage `json:"transient_payload" form:"transient_payload"` } ) @@ -72,6 +78,16 @@ func (e *Verifier) do( // already. ctx := r.Context() + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(verificationMethodSchema) + if err != nil { + return err + } + + var body transientPayloadBody + if err := e.dx.Decode(r, &body, compiler, decoderx.HTTPDecoderAllowedMethods("POST", "GET")); err != nil { + return err + } + strategy, err := e.r.GetActiveVerificationStrategy(ctx) if err != nil { return err @@ -120,7 +136,7 @@ func (e *Verifier) do( return err } - if err := strategy.SendVerificationEmail(ctx, verificationFlow, i, address); err != nil { + if err := strategy.SendVerification(ctx, verificationFlow, i, address, body.TransientPayload); err != nil { return err } diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 652df7bec61c..9f506d83cd89 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -4,7 +4,10 @@ package hook_test import ( + "bytes" "context" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/tidwall/gjson" "net/http" "net/http/httptest" "testing" @@ -26,12 +29,17 @@ import ( "github.com/ory/kratos/session" "github.com/ory/kratos/x" "github.com/ory/x/sqlxx" - "github.com/ory/x/urlx" ) func TestVerifier(t *testing.T) { ctx := context.Background() - u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + u, err := http.NewRequest( + http.MethodPost, + "https://www.ory.sh/", + bytes.NewReader([]byte("transient_payload=%7B%22branding%22%3A+%22brand-1%22%7D")), + ) + assert.NoError(t, err) + u.Header.Set("Content-Type", "application/x-www-form-urlencoded") for k, hf := range map[string]func(*hook.Verifier, *identity.Identity, flow.Flow) error{ "settings": func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { return h.ExecuteSettingsPostPersistHook( @@ -137,6 +145,74 @@ func TestVerifier(t *testing.T) { } } +func TestPhoneVerifier(t *testing.T) { + u, err := http.NewRequest( + http.MethodPost, + "https://www.ory.sh/", + bytes.NewReader([]byte("transient_payload=%7B%22branding%22%3A+%22brand-1%22%7D")), + ) + assert.NoError(t, err) + u.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + t.Run("verify phone number", func(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.phone.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"phone":"+18004444444"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + + actual, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypePhone, "+18004444444") + require.NoError(t, err) + assert.EqualValues(t, "+18004444444", actual.Value) + + var originalFlow flow.Flow + originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} + + h := hook.NewVerifier(reg) + require.NoError(t, h.ExecuteSettingsPostPersistHook(httptest.NewRecorder(), u, originalFlow.(*settings.Flow), i, + &session.Session{ID: x.NewUUID(), Identity: i})) + s, err := reg.GetActiveVerificationStrategy(ctx) + require.NoError(t, err) + expectedVerificationFlow, err := verification.NewPostHookFlow(conf, conf.SelfServiceFlowVerificationRequestLifespan(ctx), "", u, s, originalFlow) + require.NoError(t, err) + + var verificationFlow verification.Flow + require.NoError(t, reg.Persister().GetConnection(ctx).First(&verificationFlow)) + + assert.Equal(t, expectedVerificationFlow.RequestURL, verificationFlow.RequestURL) + + messages, err := reg.CourierPersister().NextMessages(ctx, 12) + require.NoError(t, err) + require.Len(t, messages, 1) + + recipients := make([]string, len(messages)) + for k, m := range messages { + recipients[k] = m.Recipient + assert.Equal(t, "brand-1", gjson.GetBytes(m.TemplateData, "TransientPayload.branding").String(), "%v", string(m.TemplateData)) + } + + assert.Equal(t, "+18004444444", messages[0].Recipient) + + //this address will be marked as sent and won't be sent again by the settings hook + address1, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypePhone, "+18004444444") + require.NoError(t, err) + assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) + + require.NoError(t, h.ExecuteSettingsPostPersistHook(httptest.NewRecorder(), u, originalFlow.(*settings.Flow), i, + &session.Session{ID: x.NewUUID(), Identity: i})) + expectedVerificationFlow, err = verification.NewPostHookFlow(conf, conf.SelfServiceFlowVerificationRequestLifespan(ctx), "", u, s, originalFlow) + var verificationFlow2 verification.Flow + require.NoError(t, reg.Persister().GetConnection(ctx).First(&verificationFlow2)) + assert.Equal(t, expectedVerificationFlow.RequestURL, verificationFlow2.RequestURL) + messages, err = reg.CourierPersister().NextMessages(ctx, 12) + require.EqualError(t, err, courier.ErrQueueEmpty.Error()) + assert.Len(t, messages, 0) + }) +} + func assertContinueWithAddresses(t *testing.T, cs []flow.ContinueWith, addresses []string) { t.Helper() diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json index fa030cbd67a0..1bcc36b12c88 100644 --- a/selfservice/strategy/code/.schema/login.schema.json +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -27,6 +27,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/.schema/verification.schema.json b/selfservice/strategy/code/.schema/verification.schema.json index 107d331972b1..493ebc7f791f 100644 --- a/selfservice/strategy/code/.schema/verification.schema.json +++ b/selfservice/strategy/code/.schema/verification.schema.json @@ -17,12 +17,20 @@ "type": "string", "format": "email" }, + "phone": { + "type": "string", + "format": "phone" + }, "flow": { "type": "string", "format": "uuid" }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/.snapshots/TestRegistration-case=registration-method=TestUpsertCodeNode.json b/selfservice/strategy/code/.snapshots/TestRegistration-case=registration-method=TestUpsertCodeNode.json new file mode 100644 index 000000000000..74aed57f4bdb --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRegistration-case=registration-method=TestUpsertCodeNode.json @@ -0,0 +1,51 @@ +{ + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "code", + "type": "text", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info", + "context": {} + } + } + } + ] +} diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json index 37f61ac9e827..2125fdccd965 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -18,6 +18,25 @@ }, "type": "input" }, + { + "attributes": { + "disabled": false, + "name": "phone", + "node_type": "input", + "required": true, + "type": "tel" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "id": 1070015, + "text": "Phone", + "type": "info" + } + }, + "type": "input" + }, { "attributes": { "disabled": false, diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index ccc3330f9ba8..5c97c130f64d 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -5,6 +5,8 @@ package code import ( "context" + "encoding/json" + "github.com/ory/x/randx" "net/url" "github.com/gofrs/uuid" @@ -66,7 +68,7 @@ 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 { +func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identity, transientPayload json.RawMessage, addresses ...Address) error { s.deps.Logger(). WithSensitiveField("address", addresses). Debugf("Preparing %s code", f.GetFlowName()) @@ -98,10 +100,22 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit return err } - emailModel := email.RegistrationCodeValidModel{ - To: address.To, - RegistrationCode: rawCode, - Traits: model, + var t courier.Template + switch address.Via { + case identity.ChannelTypeEmail: + t = email.NewRegistrationCodeValid(s.deps, &email.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Traits: model, + TransientPayload: transientPayload, + }) + case identity.ChannelTypeSMS: + t = sms.NewRegistrationCodeValid(s.deps, &sms.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Traits: model, + TransientPayload: transientPayload, + }) } s.deps.Audit(). @@ -110,7 +124,7 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit 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 { + if err := s.send(ctx, string(address.Via), t); err != nil { return errors.WithStack(err) } @@ -143,15 +157,17 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit switch address.Via { case identity.ChannelTypeEmail: t = email.NewLoginCodeValid(s.deps, &email.LoginCodeValidModel{ - To: address.To, - LoginCode: rawCode, - Identity: model, + To: address.To, + LoginCode: rawCode, + Identity: model, + TransientPayload: transientPayload, }) case identity.ChannelTypeSMS: t = sms.NewLoginCodeValid(s.deps, &sms.LoginCodeValidModel{ - To: address.To, - LoginCode: rawCode, - Identity: model, + To: address.To, + LoginCode: rawCode, + Identity: model, + TransientPayload: transientPayload, }) } @@ -251,7 +267,7 @@ func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, c // If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is // true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the // ErrUnknownAddress error. -func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, via string, to string) error { +func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, via string, to string, transientPayload json.RawMessage) error { s.deps.Logger(). WithField("via", via). WithSensitiveField("address", to). @@ -278,6 +294,11 @@ func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, } rawCode := GenerateCode() + //TODO delete after PS-187 + if via == identity.VerifiableAddressTypePhone { + rawCode = randx.MustString(4, randx.Numeric) + } + var code *VerificationCode if code, err = s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{ RawCode: rawCode, @@ -294,7 +315,7 @@ func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, return err } - if err := s.SendVerificationCodeTo(ctx, f, i, rawCode, code); err != nil { + if err := s.SendVerificationCodeTo(ctx, f, i, rawCode, code, transientPayload); err != nil { return err } return nil @@ -309,7 +330,7 @@ func (s *Sender) constructVerificationLink(ctx context.Context, fID uuid.UUID, c }).String() } -func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flow, i *identity.Identity, codeString string, code *VerificationCode) error { +func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flow, i *identity.Identity, codeString string, code *VerificationCode, transientPayload json.RawMessage) error { s.deps.Audit(). WithField("via", code.VerifiableAddress.Via). WithField("identity_id", i.ID). @@ -333,12 +354,15 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo VerificationURL: s.constructVerificationLink(ctx, f.ID, codeString), Identity: model, VerificationCode: codeString, + TransientPayload: transientPayload, }) case identity.ChannelTypeSMS: t = sms.NewVerificationCodeValid(s.deps, &sms.VerificationCodeValidModel{ To: code.VerifiableAddress.Value, + VerificationURL: s.constructVerificationLink(ctx, f.ID, codeString), VerificationCode: codeString, Identity: model, + TransientPayload: transientPayload, }) default: return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected email or sms but got %s", code.VerifiableAddress.Via)) @@ -359,12 +383,12 @@ func (s *Sender) send(ctx context.Context, via string, t courier.Template) error return err } - t, ok := t.(courier.EmailTemplate) + tmpl, ok := t.(courier.EmailTemplate) if !ok { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected email template but got %T", t)) } - _, err = c.QueueEmail(ctx, t) + _, err = c.QueueEmail(ctx, tmpl) return err case f.AddCase(identity.ChannelTypeSMS): c, err := s.deps.Courier(ctx) @@ -372,12 +396,12 @@ func (s *Sender) send(ctx context.Context, via string, t courier.Template) error return err } - t, ok := t.(courier.SMSTemplate) + tmpl, ok := t.(courier.SMSTemplate) if !ok { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected sms template but got %T", t)) } - _, err = c.QueueSMS(ctx, t) + _, err = c.QueueSMS(ctx, tmpl) return err default: return f.ToUnknownCaseErr() diff --git a/selfservice/strategy/code/code_sender_test.go b/selfservice/strategy/code/code_sender_test.go index e5ba75826eb5..335e23afed95 100644 --- a/selfservice/strategy/code/code_sender_test.go +++ b/selfservice/strategy/code/code_sender_test.go @@ -6,6 +6,7 @@ package code_test import ( "context" "encoding/base64" + "encoding/json" "fmt" "net/http" "testing" @@ -111,8 +112,8 @@ func TestSender(t *testing.T) { require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) - require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh")) - require.ErrorIs(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh"), code.ErrUnknownAddress) + require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh", json.RawMessage(`{}`))) + require.ErrorIs(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh", json.RawMessage(`{}`)), code.ErrUnknownAddress) } t.Run("case=with default templates", func(t *testing.T) { @@ -190,7 +191,7 @@ func TestSender(t *testing.T) { require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) - err = reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh") + err = reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh", json.RawMessage(`{}`)) require.ErrorIs(t, err, code.ErrUnknownAddress) }, }, diff --git a/selfservice/strategy/code/error.go b/selfservice/strategy/code/error.go new file mode 100644 index 000000000000..2b544441f377 --- /dev/null +++ b/selfservice/strategy/code/error.go @@ -0,0 +1,83 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/text" +) + +type ValidationErrorContextCodePolicyViolation struct { + Reason string +} + +type CodeSentError struct { + *schema.ValidationError +} + +func (e CodeSentError) Error() string { + return e.ValidationError.Error() +} + +func (e CodeSentError) Unwrap() error { + return e.ValidationError +} + +func (e CodeSentError) StatusCode() int { + return http.StatusOK +} + +func NewCodeSentError() error { + return CodeSentError{ + ValidationError: &schema.ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `access code has been sent`, + InstancePtr: "#/", + Context: &ValidationErrorContextCodePolicyViolation{}, + }, + Messages: new(text.Messages).Add(text.NewErrorCodeSent()), + }} +} + +func (r *ValidationErrorContextCodePolicyViolation) AddContext(_, _ string) {} + +func (r *ValidationErrorContextCodePolicyViolation) FinishInstanceContext() {} + +func NewInvalidCodeError() error { + return errors.WithStack(&schema.ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided code is invalid, check for spelling mistakes in the code or phone number`, + InstancePtr: "#/", + Context: &ValidationErrorContextCodePolicyViolation{}, + }, + Messages: new(text.Messages).Add(text.NewErrorValidationInvalidCode()), + }) +} + +func NewAttemptsExceededError() error { + return errors.WithStack(&schema.ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `maximum code verification attempts exceeded`, + InstancePtr: "#/", + Context: &ValidationErrorContextCodePolicyViolation{}, + }, + Messages: new(text.Messages).Add(text.NewErrorValidationInvalidCode()), + }) +} + +func NewSMSSpamError() error { + return errors.WithStack(&schema.ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `sms spam detected`, + InstancePtr: "#/", + Context: &ValidationErrorContextCodePolicyViolation{}, + }, + Messages: new(text.Messages).Add(text.NewErrorValidationSMSSpam()), + }) +} diff --git a/selfservice/strategy/code/extension_verify_code.go b/selfservice/strategy/code/extension_verify_code.go new file mode 100644 index 000000000000..6fc766cd01ff --- /dev/null +++ b/selfservice/strategy/code/extension_verify_code.go @@ -0,0 +1,41 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "fmt" + "sync" + + "github.com/nyaruka/phonenumbers" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/schema" +) + +type SchemaExtensionVerification struct { + l sync.Mutex + identifier string +} + +func NewSchemaExtensionVerificationCode(verifiedIdentifier string) *SchemaExtensionVerification { + return &SchemaExtensionVerification{identifier: verifiedIdentifier} +} + +func (r *SchemaExtensionVerification) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { + r.l.Lock() + defer r.l.Unlock() + + if s.Credentials.Code.Identifier { + + if phonenumbers.IsNumberMatch(fmt.Sprint(value), r.identifier) != phonenumbers.EXACT_MATCH { + return ctx.Error("", "phone number in identity traits not equal to verified phone") + } + } + + return nil +} + +func (r *SchemaExtensionVerification) Finish() error { + return nil +} diff --git a/selfservice/strategy/code/service.go b/selfservice/strategy/code/service.go new file mode 100644 index 000000000000..1a0a4a959cfa --- /dev/null +++ b/selfservice/strategy/code/service.go @@ -0,0 +1,198 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +////go:generate mockgen -destination=mocks/mock_service.go -package=mocks github.com/ory/kratos/selfservice/strategy/code Flow +// +//import ( +// "bytes" +// "context" +// "encoding/json" +// +// "github.com/gofrs/uuid" +// "github.com/hashicorp/go-retryablehttp" +// +// "github.com/ory/kratos/courier" +// templates "github.com/ory/kratos/courier/template/sms" +// "github.com/ory/kratos/driver/clock" +// "github.com/ory/kratos/driver/config" +// "github.com/ory/x/httpx" +//) +// +//type Flow interface { +// GetID() uuid.UUID +// Valid() error +//} +// +//type AuthenticationService interface { +// SendCode(ctx context.Context, flow Flow, phone string, transientPayload json.RawMessage) error +// VerifyCode(ctx context.Context, flow Flow, code string) (*Code, error) +// DoVerify(ctx context.Context, expectedCode *Code, code string) (*Code, error) +//} +// +//type dependencies interface { +// config.Provider +// clock.Provider +// CodePersistenceProvider +// courier.Provider +// courier.ConfigProvider +// HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client +// RandomCodeGeneratorProvider +//} +// +//type authenticationServiceImpl struct { +// r dependencies +//} +// +//type AuthenticationServiceProvider interface { +// CodeAuthenticationService() AuthenticationService +//} +// +//func NewCodeAuthenticationService(r dependencies) AuthenticationService { +// return &authenticationServiceImpl{r} +//} +// +//// SendCode +//// Sends a new code to the user in a message. +//// Returns error if the code was already sent and is not expired yet. +//func (s *authenticationServiceImpl) SendCode( +// ctx context.Context, +// flow Flow, +// identifier string, +// transientPayload json.RawMessage, +//) error { +// if err := flow.Valid(); err != nil { +// return err +// } +// +// codeValue := "" +// useStandbySender := false +// sendSMS := true +// for _, n := range s.r.Config().SelfServiceCodeTestNumbers(ctx) { +// if n == identifier { +// codeValue = "0000" +// sendSMS = false +// break +// } +// } +// +// if sendSMS { +// if err := s.detectSpam(ctx, identifier); err != nil { +// return err +// } +// codeValue = s.r.RandomCodeGenerator().Generate(4) +// requestStandbyConfig := s.r.Config().CourierSMSStandbyRequestConfig(ctx) +// if requestStandbyConfig != nil && bytes.Compare(requestStandbyConfig, []byte("{}")) != 0 { +// var err error +// useStandbySender, err = s.shouldUseStandbySender(ctx, identifier) +// if err != nil { +// return err +// } +// } +// } +// +// if err := s.r.CodePersister().CreateCode(ctx, &Code{ +// FlowId: flow.GetID(), +// Identifier: identifier, +// Code: codeValue, +// ExpiresAt: s.r.Clock().Now().Add(s.r.Config().SelfServiceCodeLifespan()), +// }); err != nil { +// return err +// } +// +// if sendSMS { +// cr, err := s.r.Courier(ctx) +// if err != nil { +// return err +// } +// if _, err := cr.QueueSMS( +// ctx, +// templates.NewCodeMessage( +// s.r, +// &templates.CodeMessageModel{ +// Code: codeValue, +// To: identifier, +// UseStandbySender: useStandbySender, +// TransientPayload: transientPayload, +// }), +// ); err != nil { +// return err +// } +// } +// return nil +//} +// +//// VerifyCode +//// Verifies code by looking up in db. +//func (s *authenticationServiceImpl) VerifyCode(ctx context.Context, flow Flow, code string) (*Code, error) { +// if err := flow.Valid(); err != nil { +// return nil, err +// } +// expectedCode, err := s.r.CodePersister().FindActiveCode(ctx, flow.GetID(), s.r.Clock().Now()) +// if err != nil { +// return nil, err +// } +// if expectedCode == nil { +// return nil, NewInvalidCodeError() +// } +// updatedCode, err := s.DoVerify(ctx, expectedCode, code) +// if err != nil { +// updateErr := s.r.CodePersister().UpdateCode(ctx, updatedCode) +// if updateErr != nil { +// return nil, updateErr +// } +// return updatedCode, err +// } +// +// if err = s.r.CodePersister().DeleteCodes(ctx, updatedCode.Identifier); err != nil { +// return nil, err +// } +// return updatedCode, nil +//} +// +//func (s *authenticationServiceImpl) DoVerify(ctx context.Context, expectedCode *Code, code string) (*Code, error) { +// if expectedCode.Code != code { +// expectedCode.Attempts++ +// return expectedCode, NewInvalidCodeError() +// } else if expectedCode.Attempts >= s.r.Config().SelfServiceCodeMaxAttempts() { +// return expectedCode, NewAttemptsExceededError() +// } +// return expectedCode, nil +//} +// +//func (s *authenticationServiceImpl) detectSpam(ctx context.Context, identifier string) error { +// if !s.r.Config().SelfServiceCodeSMSSpamProtectionEnabled() { +// return nil +// } +// +// count, err := s.r.CodePersister().CountByIdentifier(ctx, identifier, +// s.r.Clock().Now().AddDate(0, 0, -7)) +// if err != nil { +// return err +// } +// if count > s.r.Config().SelfServiceCodeSMSSpamProtectionMaxSingleNumber() { +// return NewSMSSpamError() +// } +// +// count, err = s.r.CodePersister().CountByIdentifierLike(ctx, identifier[0:7]+"%", +// s.r.Clock().Now().AddDate(0, 0, -7)) +// if err != nil { +// return err +// } +// if count > s.r.Config().SelfServiceCodeSMSSpamProtectionMaxNumbersRange() { +// return NewSMSSpamError() +// } +// +// return nil +//} +// +//func (s *authenticationServiceImpl) shouldUseStandbySender(ctx context.Context, identifier string) (bool, error) { +// count, err := s.r.CodePersister().CountByIdentifier(ctx, identifier, +// s.r.Clock().Now().AddDate(0, 0, -1)) +// if err != nil { +// return false, err +// } +// +// return count > 0, nil +//} diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index a4b52e980754..ed6352dfeb6e 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -167,7 +167,9 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { return err } case flow.StateEmailSent: - if err := s.populateEmailSentFlow(r.Context(), f); err != nil { + fallthrough + case flow.StateSMSSent: + if err := s.populateEmailOrSMSSentFlow(r.Context(), f); err != nil { return err } case flow.StatePassedChallenge: @@ -192,6 +194,10 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeInputEmail()), ) + f.GetUI().Nodes.Append( + node.NewInputField("phone", nil, node.CodeGroup, node.InputAttributeTypeTel, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputPhone()), + ) codeMetaLabel = text.NewInfoNodeLabelSubmit() case *login.Flow: ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(ctx) @@ -267,7 +273,7 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error return nil } -func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error { +func (s *Strategy) populateEmailOrSMSSentFlow(ctx context.Context, f flow.Flow) error { // fresh ui node group freshNodes := node.Nodes{} var route string diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index 5f7d04e395da..46894be92f80 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -6,6 +6,8 @@ package code import ( "context" "database/sql" + "encoding/json" + "fmt" "net/http" "strings" @@ -57,6 +59,8 @@ type updateLoginFlowWithCodeMethod struct { // Resend is set when the user wants to resend the code // required: false Resend string `json:"resend" form:"resend"` + + TransientPayload json.RawMessage `json:"transient_payload" form:"transient_payload"` } func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) {} @@ -187,6 +191,8 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } return nil, nil case flow.StateEmailSent: + fallthrough + case flow.StateSMSSent: i, err := s.loginVerifyCode(ctx, r, f, &p) if err != nil { return nil, s.HandleLoginError(r, f, &p, err) @@ -211,6 +217,7 @@ func (s *Strategy) loginSendCode(ctx context.Context, w http.ResponseWriter, r * var addresses []Address var i *identity.Identity + isTestNumber := false if f.RequestedAAL > identity.AuthenticatorAssuranceLevel1 { address, found := lo.Find(sess.Identity.VerifiableAddresses, func(va identity.VerifiableAddress) bool { return va.Value == p.Identifier @@ -229,9 +236,18 @@ func (s *Strategy) loginSendCode(ctx context.Context, w http.ResponseWriter, r * if err != nil { return err } + address, found := lo.Find(i.VerifiableAddresses, func(va identity.VerifiableAddress) bool { + return va.Value == p.Identifier + }) + _, isTestNumber = lo.Find(s.deps.Config().SelfServiceCodeTestNumbers(ctx), func(n string) bool { + return n == p.Identifier + }) + if !found { + return errors.WithStack(schema.NewUnknownAddressError()) + } addresses = []Address{{ To: p.Identifier, - Via: identity.CodeAddressType(identity.AddressTypeEmail), + Via: address.Via, }} } @@ -240,14 +256,21 @@ func (s *Strategy) loginSendCode(ctx context.Context, w http.ResponseWriter, r * return errors.WithStack(err) } - // 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) + if !isTestNumber { + if err := s.deps.CodeSender().SendCode(ctx, f, i, p.TransientPayload, addresses...); err != nil { + return errors.WithStack(err) + } } // sets the flow state to code sent - f.SetState(flow.NextState(f.GetState())) + switch addresses[0].Via { + case identity.ChannelTypeEmail: + f.SetState(flow.StateEmailSent) + case identity.ChannelTypeSMS: + f.SetState(flow.StateSMSSent) + default: + return fmt.Errorf("Unexpected address Via: %v", addresses[0]) + } if err := s.NewCodeUINodes(r, f, &codeIdentifier{Identifier: p.Identifier}); err != nil { return err @@ -299,12 +322,27 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi return nil, err } - loginCode, err := s.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) - if err != nil { - if errors.Is(err, ErrCodeNotFound) { + _, isTestNumber := lo.Find(s.deps.Config().SelfServiceCodeTestNumbers(ctx), func(n string) bool { + return n == p.Identifier + }) + + var loginCode *LoginCode + if isTestNumber { + if p.Code != "0000" { return nil, schema.NewLoginCodeInvalid() } - return nil, errors.WithStack(err) + loginCode = &LoginCode{ + IdentityID: i.ID, + Address: p.Identifier, + } + } else { + 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) @@ -347,6 +385,7 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, &va); err != nil { return nil, err } + i.VerifiableAddresses[idx] = va break } } diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index 06d811362978..4a932a9daf53 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -7,10 +7,15 @@ import ( "context" "encoding/json" "fmt" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/x/assertx" + "math/rand" "net/http" "net/http/httptest" "net/url" "testing" + "time" "github.com/ory/x/ioutilx" "github.com/ory/x/snapshotx" @@ -741,3 +746,239 @@ func TestLoginCodeStrategy(t *testing.T) { }) } } + +func TestLoginCodeStrategy_SMS(t *testing.T) { + newReturnTs := func(t *testing.T, reg interface { + session.ManagementProvider + x.WriterProvider + config.Provider + }) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sess, err := reg.SessionManager().FetchFromRequest(r.Context(), r) + require.NoError(t, err) + reg.Writer().Write(w, r, sess) + })) + t.Cleanup(ts.Close) + reg.Config().MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/return-ts") + return ts + } + + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypePassword.String()), false) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + //conf.MustSet(ctx, config.CodeMaxAttempts, 5) + //conf.MustSet(ctx, config.CodeLifespan, "1h") + + publicTS, _ := testhelpers.NewKratosServer(t, reg) + redirTS := newReturnTs(t, reg) + + uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts") + + var expectValidationError = func(t *testing.T, isAPI, forced, isSPA bool, values func(url.Values)) string { + return testhelpers.SubmitLoginForm(t, isAPI, nil, publicTS, values, + isSPA, forced, + testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+login.RouteSubmitFlow, conf.SelfServiceFlowLoginUI(ctx).String()), + ) + } + + createIdentity := func(identifier string) (error, *identity.Identity) { + stateChangedAt := sqlxx.NullTime(time.Now()) + + i := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(fmt.Sprintf(`{"phone":"%s"}`, identifier)), + State: identity.StateActive, + StateChangedAt: &stateChangedAt} + if err := reg.IdentityManager().Create(ctx, i); err != nil { + return err, nil + } + + return nil, i + } + + getLoginNode := func(f *oryClient.LoginFlow, nodeName string) *oryClient.UiNode { + for _, n := range f.Ui.Nodes { + if n.Attributes.UiNodeInputAttributes.Name == nodeName { + return &n + } + } + return nil + } + + var loginWithPhone = func( + t *testing.T, isAPI, refresh, isSPA bool, + expectedStatusCode int, expectedURL string, + values func(url.Values), + ) string { + f := testhelpers.InitializeLoginFlow(t, isAPI, nil, publicTS, false, false) + + assert.Empty(t, getLoginNode(f, "code")) + assert.NotEmpty(t, getLoginNode(f, "identifier")) + + body := testhelpers.SubmitLoginFormWithFlow(t, isAPI, nil, values, + false, testhelpers.ExpectStatusCode(isAPI, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+login.RouteSubmitFlow, conf.SelfServiceFlowLoginUI(ctx).String()), + f) + + assertx.EqualAsJSON(t, + text.NewLoginEmailWithCodeSent(), + json.RawMessage(gjson.Get(body, "ui.messages.0").Raw), + "%s", body, + ) + assert.Equal(t, flow.StateSMSSent.String(), gjson.Get(body, "state").String(), + "%s", testhelpers.PrettyJSON(t, []byte(body))) + assert.Equal(t, "code", gjson.Get(body, "active").String(), "%s", body) + assert.NotEmpty(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)"), "%s", body) + assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==code).attirbutes.value"), "%s", body) + + st := gjson.Get(body, "session_token").String() + assert.Empty(t, st, "Response body: %s", body) //No session token as we have not presented the code yet + + values = func(v url.Values) { + v.Del("resend") + if isAPI { + v.Set("method", "code") + } + v.Set("code", "0000") + } + + publicClient := testhelpers.NewSDKCustomClient(publicTS, &http.Client{}) + f, _, err := publicClient.FrontendApi.GetLoginFlow(ctx).Id(f.Id).Execute() + assert.NoError(t, err) + body = testhelpers.SubmitLoginFormWithFlow(t, isAPI, nil, values, false, expectedStatusCode, expectedURL, f) + + return body + } + + t.Run("should return an error because no phone is set", func(t *testing.T) { + var check = func(t *testing.T, body string) { + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) + + assert.Equal(t, "Property identifier is missing.", + gjson.Get(body, "ui.nodes.#(attributes.name==identifier).messages.0.text").String(), "%s", body) + + // The code value should not be returned! + assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==code).attributes.value").String()) + } + + t.Run("type=api", func(t *testing.T) { + var values = func(v url.Values) { + v.Set("method", "code") + v.Del("identifier") + } + + check(t, expectValidationError(t, true, false, false, values)) + }) + }) + + t.Run("should not send code as user was not registered", func(t *testing.T) { + var check = func(t *testing.T, isAPI bool, values func(url.Values)) { + f := testhelpers.InitializeLoginFlow(t, isAPI, nil, publicTS, false, false) + body := testhelpers.SubmitLoginFormWithFlow(t, isAPI, nil, values, + false, testhelpers.ExpectStatusCode(isAPI, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI, publicTS.URL+login.RouteSubmitFlow, conf.SelfServiceFlowLoginUI(ctx).String()), + f) + + assertx.EqualAsJSON(t, + text.NewErrorValidationNoCodeUser(), + json.RawMessage(gjson.Get(body, "ui.messages.0").Raw), + "%s", body, + ) + } + + t.Run("type=api", func(t *testing.T) { + var values = func(v url.Values) { + v.Set("method", "code") + v.Set("identifier", "+99999999999") + } + + check(t, true, values) + }) + t.Run("type=browser", func(t *testing.T) { + var values = func(v url.Values) { + v.Set("identifier", "+99999999999") + } + + check(t, false, values) + }) + }) + + t.Run("should pass with registered user", func(t *testing.T) { + identifier := fmt.Sprintf("+452%s", fmt.Sprint(rand.Int())[0:7]) + conf.MustSet(ctx, config.CodeTestNumbers, []string{identifier}) + err, createdIdentity := createIdentity(identifier) + assert.NoError(t, err) + + var values = func(v url.Values) { + v.Set("method", "code") + v.Set("identifier", identifier) + } + + t.Run("type=api", func(t *testing.T) { + body := loginWithPhone(t, true, false, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow, values) + assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.phone").String(), "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, createdIdentity.ID) + assert.NoError(t, err) + assert.NotEmpty(t, i.VerifiableAddresses, "%s", body) + assert.Equal(t, identifier, i.VerifiableAddresses[0].Value) + assert.True(t, i.VerifiableAddresses[0].Verified) + + assert.Equal(t, identifier, gjson.Get(body, "session.identity.verifiable_addresses.0.value").String()) + assert.Equal(t, "true", gjson.Get(body, "session.identity.verifiable_addresses.0.verified").String(), "%s", body) + }) + t.Run("type=browser", func(t *testing.T) { + body := loginWithPhone(t, false, false, false, http.StatusOK, redirTS.URL, values) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.phone").String(), "%s", body) + assert.True(t, gjson.Get(body, "active").Bool(), "%s", body) + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, createdIdentity.ID) + assert.NoError(t, err) + assert.NotEmpty(t, i.VerifiableAddresses, "%s", body) + assert.Equal(t, identifier, i.VerifiableAddresses[0].Value) + assert.True(t, i.VerifiableAddresses[0].Verified) + + assert.Equal(t, identifier, gjson.Get(body, "identity.verifiable_addresses.0.value").String()) + assert.Equal(t, "true", gjson.Get(body, "identity.verifiable_addresses.0.verified").String()) + }) + }) + + t.Run("should save transient payload to SMS template data", func(t *testing.T) { + identifier := fmt.Sprintf("+452%s", fmt.Sprint(rand.Int())[0:7]) + err, _ := createIdentity(identifier) + assert.NoError(t, err) + + var values = func(v url.Values) { + v.Set("method", "code") + v.Set("identifier", identifier) + v.Set("transient_payload", `{"branding": "brand-1"}`) + } + + var doTest = func(t *testing.T, isAPI bool) { + f := testhelpers.InitializeLoginFlow(t, isAPI, nil, publicTS, false, false) + testhelpers.SubmitLoginFormWithFlow(t, isAPI, nil, values, + false, testhelpers.ExpectStatusCode(isAPI, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI, publicTS.URL+login.RouteSubmitFlow, conf.SelfServiceFlowLoginUI(ctx).String()), + f) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, identifier, "") + assert.Equal(t, "brand-1", gjson.GetBytes(message.TemplateData, "TransientPayload.branding").String(), "%s", message.TemplateData) + } + + t.Run("type=browser", func(t *testing.T) { + doTest(t, false) + }) + + t.Run("type=api", func(t *testing.T) { + doTest(t, true) + }) + }) + +} diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index 4d728426348f..6f2372cd0cfd 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -7,6 +7,8 @@ import ( "context" "database/sql" "encoding/json" + "fmt" + "github.com/samber/lo" "net/http" "github.com/ory/herodot" @@ -163,6 +165,8 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat case flow.StateChooseMethod: return s.HandleRegistrationError(ctx, r, f, &p, s.registrationSendEmail(ctx, w, r, f, &p, i)) case flow.StateEmailSent: + fallthrough + case flow.StateSMSSent: 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())) @@ -182,7 +186,7 @@ func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWri // Create the Registration code // Step 1: validate the identity's traits - cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + _, err = s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) if err != nil { return err } @@ -192,19 +196,37 @@ func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWri 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}) + // Step 3: Get the identifier and send the code + cred, found := i.GetCredentials(identity.CredentialsTypeCodeAuth) + if !found { + return fmt.Errorf("credentials not found") + } + if len(cred.Identifiers) != 1 { + return fmt.Errorf("credentials identifiers missing or more than one: %v", cred.Identifiers) + } + address, found := lo.Find(i.VerifiableAddresses, func(va identity.VerifiableAddress) bool { + return va.Value == cred.Identifiers[0] + }) + if !found { + return errors.WithStack(schema.NewUnknownAddressError()) } - // 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 { + addresses := []Address{{ + To: cred.Identifiers[0], + Via: address.Via, + }} + if err := s.deps.CodeSender().SendCode(ctx, f, i, json.RawMessage("{}"), addresses...); err != nil { return errors.WithStack(err) } // sets the flow state to code sent - f.SetState(flow.NextState(f.GetState())) + switch address.Via { + case identity.ChannelTypeEmail: + f.SetState(flow.StateEmailSent) + case identity.ChannelTypeSMS: + f.SetState(flow.StateSMSSent) + default: + return fmt.Errorf("Unexpected address Via: %v", address) + } // Step 4: Generate the UI for the `code` input form // re-initialize the UI with a "clean" new state diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index 1ca150b5f0ba..6750069406c2 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -7,12 +7,20 @@ import ( "context" "encoding/json" "fmt" + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/snapshotx" + "github.com/ory/x/urlx" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" + "time" _ "embed" @@ -603,3 +611,306 @@ func TestRegistrationCodeStrategy(t *testing.T) { } }) } + +func TestRegistrationCode_SMS(t *testing.T) { + ctx := context.Background() + + cleanCourierQueue := func(reg *driver.RegistryDefault) { + for { + _, err := reg.CourierPersister().NextMessages(context.Background(), 10) + if err != nil { + return + } + } + } + + //requestCode := func(t *testing.T, publicTS *httptest.Server, identifier string, statusCode int) { + // hc := new(http.Client) + // f := testhelpers.InitializeRegistrationFlow(t, true, hc, publicTS, false) + // var values = func(v url.Values) { + // v.Set("method", "code") + // v.Set("traits.phone", identifier) + // } + // testhelpers.SubmitRegistrationFormWithFlow(t, true, hc, values, + // false, statusCode, publicTS.URL+registration.RouteSubmitFlow, f) + //} + + getRegistrationNode := func(f *oryClient.RegistrationFlow, nodeName string) *oryClient.UiNode { + for _, n := range f.Ui.Nodes { + if n.Attributes.UiNodeInputAttributes.Name == nodeName { + return &n + } + } + return nil + } + + t.Run("case=registration", func(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + + router := x.NewRouterPublic() + admin := x.NewRouterAdmin() + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) + + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, admin) + //errTS := testhelpers.NewErrorTestServer(t, reg) + //uiTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + // Overwrite these two to ensure that they run + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/default-return-to") + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, redirTS.URL+"/registration-return-ts") + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + //conf.MustSet(ctx, config.CodeMaxAttempts, 5) + + t.Run("case=should fail if identifier changed when submitted with code", func(t *testing.T) { + identifier := "+4550050000" + + cleanCourierQueue(reg) + + hc := new(http.Client) + + f := testhelpers.InitializeRegistrationFlow(t, true, hc, publicTS, false) + + var values = func(v url.Values) { + v.Set("method", "code") + v.Set("traits.phone", identifier) + } + body := testhelpers.SubmitRegistrationFormWithFlow(t, true, hc, values, + false, http.StatusBadRequest, publicTS.URL+registration.RouteSubmitFlow, f) + assertx.EqualAsJSON(t, + text.NewRegistrationEmailWithCodeSent(), + json.RawMessage(gjson.Get(body, "ui.messages.0").Raw), + "%s", testhelpers.PrettyJSON(t, []byte(body)), + ) + assert.Equal(t, flow.StateSMSSent.String(), gjson.Get(body, "state").String(), + "%s", testhelpers.PrettyJSON(t, []byte(body))) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, identifier, "") + var smsModel sms.RegistrationCodeValidModel + err := json.Unmarshal(message.TemplateData, &smsModel) + assert.NoError(t, err) + + values = func(v url.Values) { + v.Set("method", "code") + v.Set("traits.phone", identifier+"2") + v.Set("code", smsModel.RegistrationCode) + } + + body = testhelpers.SubmitRegistrationFormWithFlow(t, true, hc, values, + false, http.StatusBadRequest, publicTS.URL+registration.RouteSubmitFlow, f) + assertx.EqualAsJSON(t, + text.NewErrorValidationTraitsMismatch(), + json.RawMessage(gjson.Get(body, "ui.messages.0").Raw), + "%s", testhelpers.PrettyJSON(t, []byte(body)), + ) + }) + + //t.Run("case=should fail if spam detected", func(t *testing.T) { + // identifier := "+4550050001" + // conf.MustSet(ctx, config.CodeSMSSpamProtectionEnabled, true) + // + // for i := 0; i <= 50; i++ { + // requestCode(t, publicTS, identifier, http.StatusOK) + // } + // + // requestCode(t, publicTS, identifier, http.StatusBadRequest) + // + // identifier = "+456005" + // + // for i := 0; i <= 100; i++ { + // requestCode(t, publicTS, identifier+fmt.Sprintf("%04d", i), http.StatusOK) + // } + // + // requestCode(t, publicTS, identifier+"0101", http.StatusBadRequest) + //}) + + var expectSuccessfulLogin = func( + t *testing.T, isAPI, isSPA bool, hc *http.Client, + expectReturnTo string, + identifier string, + ) string { + if hc == nil { + if isAPI { + hc = new(http.Client) + } else { + hc = testhelpers.NewClientWithCookies(t) + } + } + + cleanCourierQueue(reg) + + f := testhelpers.InitializeRegistrationFlow(t, isAPI, hc, publicTS, isSPA) + + assert.Empty(t, getRegistrationNode(f, "code")) + assert.NotEmpty(t, getRegistrationNode(f, "traits.phone")) + + var values = func(v url.Values) { + v.Set("method", "code") + v.Set("traits.phone", identifier) + } + body := testhelpers.SubmitRegistrationFormWithFlow(t, isAPI, hc, values, + isSPA, testhelpers.ExpectStatusCode(isAPI, http.StatusBadRequest, http.StatusOK), expectReturnTo, f) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 10) + assert.NoError(t, err, "Courier queue should not be empty.") + assert.Equal(t, 1, len(messages)) + var smsModel sms.RegistrationCodeValidModel + err = json.Unmarshal(messages[0].TemplateData, &smsModel) + assert.NoError(t, err) + + st := gjson.Get(body, "session_token").String() + assert.Empty(t, st, "Response body: %s", body) //No session token as we have not presented the code yet + + values = func(v url.Values) { + v.Set("method", "code") + v.Set("traits.phone", identifier) + v.Set("code", smsModel.RegistrationCode) + } + + body = testhelpers.SubmitRegistrationFormWithFlow(t, isAPI, hc, values, + isSPA, http.StatusOK, expectReturnTo, f) + + assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.phone").String(), + "%s", body) + identityID, err := uuid.FromString(gjson.Get(body, "identity.id").String()) + assert.NoError(t, err) + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), identityID) + assert.NoError(t, err) + assert.NotEmpty(t, i.Credentials, "%s", body) + assert.Equal(t, identifier, i.Credentials["code"].Identifiers[0], "%s", body) + assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) + assert.Equal(t, identifier, gjson.Get(body, "identity.verifiable_addresses.0.value").String()) + assert.Equal(t, "true", gjson.Get(body, "identity.verifiable_addresses.0.verified").String()) + + return body + } + + t.Run("case=should pass and set up a session", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeCodeAuth.String()), []config.SelfServiceHook{{Name: "session"}}) + t.Cleanup(func() { + conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeCodeAuth.String()), nil) + }) + + identifier := "+4570050001" + + t.Run("type=api", func(t *testing.T) { + expectSuccessfulLogin(t, true, false, nil, + publicTS.URL+registration.RouteSubmitFlow, identifier) + }) + + //t.Run("type=spa", func(t *testing.T) { + // hc := testhelpers.NewClientWithCookies(t) + // body := expectSuccessfulLogin(t, false, true, hc, func(v url.Values) { + // v.Set("traits.username", "registration-identifier-8-spa") + // v.Set("password", x.NewUUID().String()) + // v.Set("traits.foobar", "bar") + // }) + // assert.Equal(t, `registration-identifier-8-spa`, gjson.Get(body, "identity.traits.username").String(), "%s", body) + // assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) + // assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + //}) + // + //t.Run("type=browser", func(t *testing.T) { + // body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + // v.Set("traits.username", "registration-identifier-8-browser") + // v.Set("password", x.NewUUID().String()) + // v.Set("traits.foobar", "bar") + // }) + // assert.Equal(t, `registration-identifier-8-browser`, gjson.Get(body, "identity.traits.username").String(), "%s", body) + //}) + }) + + t.Run("case=should create verifiable address", func(t *testing.T) { + identifier := "+1234567890" + conf.MustSet(ctx, config.CodeTestNumbers, []string{identifier}) + createdIdentity := &identity.Identity{ + SchemaID: "default", + Traits: identity.Traits(fmt.Sprintf(`{"phone":"%s"}`, identifier)), + State: identity.StateActive} + err := reg.IdentityManager().Create(context.Background(), createdIdentity) + assert.NoError(t, err) + + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), createdIdentity.ID) + assert.NoError(t, err) + assert.Equal(t, identifier, i.VerifiableAddresses[0].Value) + assert.False(t, i.VerifiableAddresses[0].Verified) + assert.Equal(t, identity.VerifiableAddressStatusPending, i.VerifiableAddresses[0].Status) + }) + + t.Run("method=TestPopulateSignUpMethod", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, publicTS.URL) + }) + + sr, err := registration.NewFlow(conf, time.Minute, "nosurf", &http.Request{URL: urlx.ParseOrPanic("/")}, flow.TypeBrowser) + require.NoError(t, err) + require.NoError(t, reg.RegistrationStrategies(context.Background()). + MustStrategy(identity.CredentialsTypeCodeAuth).(*code.Strategy).PopulateRegistrationMethod(&http.Request{}, sr)) + + snapshotx.SnapshotTExcept(t, sr.UI, []string{"action", "nodes.3.attributes.value"}) + }) + + //t.Run("case=should use standby sender", func(t *testing.T) { + // senderMessagesCount := 0 + // standbySenderMessagesCount := 0 + // senderSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // senderMessagesCount++ + // })) + // standbySenderSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // standbySenderMessagesCount++ + // })) + // + // //configTemplate := `{ + // // "url": "%s", + // // "method": "POST", + // // "body": "file://./stub/request.config.twilio.jsonnet" + // //}` + // + // //conf.MustSet(ctx, config.ViperKeyCourierSMSRequestConfig, fmt.Sprintf(configTemplate, senderSrv.URL)) + // //conf.MustSet(ctx, config.ViperKeyCourierSMSStandbyRequestConfig, fmt.Sprintf(configTemplate, standbySenderSrv.URL)) + // //conf.MustSet(ctx, config.ViperKeyCourierSMSFrom, "test sender") + // //conf.MustSet(ctx, config.ViperKeyCourierSMSStandbyFrom, "test standby sender") + // //conf.MustSet(ctx, config.ViperKeyCourierSMSEnabled, true) + // conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "http://foo.url") + // + // c, err := reg.Courier(ctx) + // require.NoError(t, err) + // + // ctx, cancel := context.WithCancel(ctx) + // defer t.Cleanup(cancel) + // + // identifier := "+4550050005" + // f := testhelpers.InitializeRegistrationFlow(t, true, nil, publicTS, false) + // var values = func(v url.Values) { + // v.Set("method", "code") + // v.Set("traits.phone", identifier) + // } + // testhelpers.SubmitRegistrationFormWithFlow(t, true, nil, values, + // false, http.StatusOK, publicTS.URL+registration.RouteSubmitFlow, f) + // testhelpers.SubmitRegistrationFormWithFlow(t, true, nil, values, + // false, http.StatusOK, publicTS.URL+registration.RouteSubmitFlow, f) + // + // go func() { + // require.NoError(t, c.Work(ctx)) + // }() + // + // require.NoError(t, resilience.Retry(reg.Logger(), time.Millisecond*250, time.Second*10, func() error { + // if senderMessagesCount+standbySenderMessagesCount >= 2 { + // return nil + // } + // return errors.New("messages not sent yet") + // })) + // + // assert.Equal(t, 1, senderMessagesCount) + // assert.Equal(t, 1, standbySenderMessagesCount) + // + // senderSrv.Close() + // standbySenderSrv.Close() + // + //}) + + }) +} diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index e6969fda738d..21949ace0dff 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -5,6 +5,8 @@ package code import ( "context" + "encoding/json" + "github.com/ory/x/randx" "net/http" "time" @@ -71,6 +73,9 @@ func (s *Strategy) handleVerificationError(w http.ResponseWriter, r *http.Reques f.UI.GetNodes().Upsert( node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) + f.UI.GetNodes().Upsert( + node.NewInputField("phone", body.Phone, node.CodeGroup, node.InputAttributeTypeTel, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputPhone()), + ) } return err @@ -91,6 +96,10 @@ type updateVerificationFlowWithCodeMethod struct { // required: false Email string `form:"email" json:"email"` + // Phone to Verify + // format: tel + Phone string `form:"phone" json:"phone"` + // Sending the anti-csrf token is only required for browser login flows. CSRFToken string `form:"csrf_token" json:"csrf_token"` @@ -110,6 +119,8 @@ type updateVerificationFlowWithCodeMethod struct { // The id of the flow Flow string `json:"-" form:"-"` + + TransientPayload json.RawMessage `json:"transient_payload" form:"transient_payload"` } // getMethod returns the method of this submission or "" if no method could be found @@ -146,6 +157,8 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio case flow.StateChooseMethod: fallthrough case flow.StateEmailSent: + fallthrough + case flow.StateSMSSent: return s.verificationHandleFormSubmission(w, r, f, body) case flow.StatePassedChallenge: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) @@ -187,8 +200,8 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht // If not GET: try to use the submitted code return s.verificationUseCode(w, r, body.Code, f) - } else if len(body.Email) == 0 { - // If no code and no email was provided, fail with a validation error + } else if len(body.Email) == 0 && len(body.Phone) == 0 { + // If no code and no email or phone was provided, fail with a validation error return s.handleVerificationError(w, r, f, body, schema.NewRequiredError("#/email", "email")) } @@ -196,18 +209,32 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht return s.handleVerificationError(w, r, f, body, err) } + via := identity.VerifiableAddressTypeEmail + to := body.Email + if len(body.Phone) != 0 { + via = identity.VerifiableAddressTypePhone + to = body.Phone + } + if err := s.deps.VerificationCodePersister().DeleteVerificationCodesOfFlow(r.Context(), f.ID); err != nil { return s.handleVerificationError(w, r, f, body, err) } - if err := s.deps.CodeSender().SendVerificationCode(r.Context(), f, identity.VerifiableAddressTypeEmail, body.Email); err != nil { + if err := s.deps.CodeSender().SendVerificationCode(r.Context(), f, via, to, body.TransientPayload); err != nil { if !errors.Is(err, ErrUnknownAddress) { return s.handleVerificationError(w, r, f, body, err) } // Continue execution } - f.State = flow.StateEmailSent + switch via { + case identity.VerifiableAddressTypeEmail: + f.State = flow.StateEmailSent + case identity.VerifiableAddressTypePhone: + f.State = flow.StateSMSSent + default: + return errors.New("Unexpected via: " + via) + } if err := s.PopulateVerificationMethod(r, f); err != nil { return s.handleVerificationError(w, r, f, body, err) @@ -274,7 +301,11 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c 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()) + if code.VerifiableAddress.Via == identity.VerifiableAddressTypeEmail { + f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) + } else { + f.UI.Messages.Set(text.NewInfoSelfServicePhoneVerificationSuccessful()) + } f.UI. Nodes. Append(node.NewAnchorField("continue", returnTo.String(), node.CodeGroup, text.NewInfoNodeLabelContinue()). @@ -354,8 +385,13 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress) (err error) { +func (s *Strategy) SendVerification(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress, transientPayload json.RawMessage) (err error) { + rawCode := GenerateCode() + //TODO delete after PS-187 + if a.Via == identity.VerifiableAddressTypePhone { + rawCode = randx.MustString(4, randx.Numeric) + } code, err := s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{ RawCode: rawCode, @@ -367,5 +403,5 @@ func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Fl return err } - return s.deps.CodeSender().SendVerificationCodeTo(ctx, f, i, rawCode, code) + return s.deps.CodeSender().SendVerificationCodeTo(ctx, f, i, rawCode, code, transientPayload) } diff --git a/selfservice/strategy/code/strategy_verification_phone_test.go b/selfservice/strategy/code/strategy_verification_phone_test.go new file mode 100644 index 000000000000..e7e9451b40ec --- /dev/null +++ b/selfservice/strategy/code/strategy_verification_phone_test.go @@ -0,0 +1,175 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/courier/template/sms" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + client "github.com/ory/kratos/internal/httpclient" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" +) + +func TestPhoneVerification(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+identity.CredentialsTypePassword.String()+".enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(recovery.RecoveryStrategyLink)+".enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(recovery.RecoveryStrategyCode)+".enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryEnabled, true) + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + + public, _ := testhelpers.NewKratosServerWithCSRF(t, reg) + _ = testhelpers.NewVerificationUIFlowEchoServer(t, reg) + + var identityToVerify = &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"phone":"+4580010000"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } + + var verificationPhone = gjson.GetBytes(identityToVerify.Traits, "phone").String() + + require.NoError(t, reg.IdentityManager().Create(ctx, identityToVerify, + identity.ManagerAllowWriteProtectedTraits)) + + var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int, + f *client.VerificationFlow) string { + if hc == nil { + hc = testhelpers.NewDebugClient(t) + if !isAPI { + hc = testhelpers.NewClientWithCookies(t) + hc.Transport = testhelpers.NewTransportWithLogger(http.DefaultTransport, t).RoundTripper + } + } + + return testhelpers.SubmitVerificationForm(t, isAPI, isSPA, hc, public, values, c, + testhelpers.ExpectURL(isAPI || isSPA, + public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String()), + f) + } + + var expectSuccess = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, + f *client.VerificationFlow, values func(url.Values)) string { + return expect(t, hc, isAPI, isSPA, values, http.StatusOK, f) + } + + t.Run("description=should not be able to use an invalid code", func(t *testing.T) { + // phones should be verified with code regardless of the 'verification.use' setting + for _, verificationUse := range []string{"code", "link"} { + t.Run("verification.use="+verificationUse, func(t *testing.T) { + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeVerificationFlowViaAPI(t, nil, public) + body := expectSuccess(t, nil, true, false, f, + func(v url.Values) { + v.Set("method", "code") + v.Set("phone", verificationPhone) + }) + assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(body, "ui.messages.0").Raw)) + + body = expect(t, nil, true, false, + func(v url.Values) { + v.Set("method", "code") + v.Set("phone", verificationPhone) + v.Set("code", "invalid_code") + }, + http.StatusOK, f) + assertx.EqualAsJSON(t, text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), + json.RawMessage(gjson.Get(body, "ui.messages.0").Raw), "%s", body) + }) + }) + } + }) + + t.Run("description=should verify phone", func(t *testing.T) { + t.Run("verification.use=code", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationUse, "code") + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeVerificationFlowViaAPI(t, nil, public) + body := expectSuccess(t, nil, true, false, f, + func(v url.Values) { + v.Set("method", "code") + v.Set("phone", verificationPhone) + }) + assert.EqualValues(t, "code", gjson.Get(body, "active").String(), "%s", body) + assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(body, "ui.messages.0").Raw)) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationPhone, "") + + var smsModel sms.VerificationCodeValidModel + err := json.Unmarshal(message.TemplateData, &smsModel) + assert.NoError(t, err) + + body = expectSuccess(t, nil, true, false, f, + func(v url.Values) { + v.Set("method", "code") + v.Set("phone", verificationPhone) + v.Set("code", smsModel.VerificationCode) + }) + assert.EqualValues(t, "code", gjson.Get(body, "active").String(), "%s", body) + + assert.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String(), "%s", body) + assert.EqualValues(t, text.NewInfoSelfServicePhoneVerificationSuccessful().Text, + gjson.Get(body, "ui.messages.0.text").String()) + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identityToVerify.ID) + require.NoError(t, err) + require.Len(t, id.VerifiableAddresses, 1) + + address := id.VerifiableAddresses[0] + assert.EqualValues(t, verificationPhone, address.Value) + assert.True(t, address.Verified) + assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, address.Status) + assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now())) + }) + }) + }) + + t.Run("description=should save transient payload to template data", func(t *testing.T) { + var doTest = func(t *testing.T, client *http.Client, isAPI bool, f *client.VerificationFlow) { + expectSuccess(t, client, isAPI, false, f, + func(v url.Values) { + v.Set("method", "code") + v.Set("phone", verificationPhone) + v.Set("transient_payload", `{"branding": "brand-1"}`) + }) + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationPhone, "") + assert.Equal(t, "brand-1", gjson.GetBytes(message.TemplateData, "TransientPayload.branding").String(), "%s", message.TemplateData) + } + + for _, verificationUse := range []string{"code", "link"} { + t.Run("verification.use="+verificationUse, func(t *testing.T) { + t.Run("type=browser", func(t *testing.T) { + c := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeVerificationFlowViaBrowser(t, c, false, public) + doTest(t, c, false, f) + }) + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeVerificationFlowViaAPI(t, nil, public) + doTest(t, nil, true, f) + }) + }) + } + }) +} diff --git a/selfservice/strategy/code/strategy_verification_test.go b/selfservice/strategy/code/strategy_verification_test.go index 0f6a9fe15a10..45907a293800 100644 --- a/selfservice/strategy/code/strategy_verification_test.go +++ b/selfservice/strategy/code/strategy_verification_test.go @@ -81,7 +81,8 @@ func TestVerification(t *testing.T) { } return testhelpers.SubmitVerificationForm(t, isAPI, isSPA, hc, public, values, c, - testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) + testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String()), + nil) } expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { @@ -117,7 +118,7 @@ func TestVerification(t *testing.T) { c := testhelpers.NewClientWithCookies(t) rs := testhelpers.GetVerificationFlow(t, c, public) - testhelpers.SnapshotTExcept(t, rs.Ui.Nodes, []string{"2.attributes.value"}) + testhelpers.SnapshotTExcept(t, rs.Ui.Nodes, []string{"3.attributes.value"}) assert.EqualValues(t, public.URL+verification.RouteSubmitFlow+"?flow="+rs.Id, rs.Ui.Action) assert.Empty(t, rs.Ui.Messages) }) @@ -229,7 +230,7 @@ func TestVerification(t *testing.T) { c := testhelpers.NewClientWithCookies(t) f := testhelpers.SubmitVerificationForm(t, false, false, c, public, func(v url.Values) { v.Set("email", verificationEmail) - }, 200, "") + }, 200, "", nil) fID := gjson.Get(f, "id").String() res, err := c.Get(public.URL + verification.RouteSubmitFlow + "?flow=" + fID + "&code=12312312") require.NoError(t, err) @@ -245,7 +246,7 @@ func TestVerification(t *testing.T) { c := testhelpers.NewClientWithCookies(t) f := testhelpers.SubmitVerificationForm(t, false, false, c, public, func(v url.Values) { v.Set("email", verificationEmail) - }, 200, "") + }, 200, "", nil) body, res := submitVerificationCode(t, f, c, "12312312") assert.Equal(t, http.StatusOK, res.StatusCode) diff --git a/selfservice/strategy/code/stub/default.schema.json b/selfservice/strategy/code/stub/default.schema.json index 8dc923266050..696d0f99f396 100644 --- a/selfservice/strategy/code/stub/default.schema.json +++ b/selfservice/strategy/code/stub/default.schema.json @@ -22,8 +22,27 @@ "via": "email" } } + }, + "phone": { + "type": "string", + "format": "phone", + "minLength": 11, + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + }, + "verification": { + "via": "sms" + } + } } - } + }, + "required": [ + ] } - } + }, + "additionalProperties": false } diff --git a/selfservice/strategy/code/stub/request.config.twilio.jsonnet b/selfservice/strategy/code/stub/request.config.twilio.jsonnet new file mode 100644 index 000000000000..da0736b06df0 --- /dev/null +++ b/selfservice/strategy/code/stub/request.config.twilio.jsonnet @@ -0,0 +1,5 @@ +function(ctx) { + from: ctx.from, + to: ctx.to, + body: ctx.body +} diff --git a/selfservice/strategy/code/types.go b/selfservice/strategy/code/types.go new file mode 100644 index 000000000000..f74e2a8ce1b4 --- /dev/null +++ b/selfservice/strategy/code/types.go @@ -0,0 +1,25 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import "encoding/json" + +// submitSelfServiceLoginFlowWithPasswordMethod is used to decode the login form payload. +// +// swagger:model submitSelfServiceLoginFlowWithCodeMethod +type submitSelfServiceLoginFlowWithCodeMethod struct { + // Method should be set to "code" when logging in using the code strategy. + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // The user's phone number. + Identifier string `json:"identifier"` + + // One-time code. + Code string `json:"code"` + + TransientPayload json.RawMessage `json:"transient_payload" form:"transient_payload"` +} diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index 0db56233fa3f..3c13068fce6d 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -5,6 +5,7 @@ package link import ( "context" + "encoding/json" "net/http" "net/url" "time" @@ -309,7 +310,7 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress) error { +func (s *Strategy) SendVerification(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress, transientPayload json.RawMessage) 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 c81834e32b91..68e390b5022a 100644 --- a/selfservice/strategy/link/strategy_verification_test.go +++ b/selfservice/strategy/link/strategy_verification_test.go @@ -73,7 +73,8 @@ func TestVerification(t *testing.T) { } return testhelpers.SubmitVerificationForm(t, isAPI, isSPA, hc, public, values, c, - testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) + testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String()), + nil) } expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index 8d8879cce91c..c3be3486bfb6 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -79,6 +79,8 @@ func TestCompleteLogin(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), + map[string]interface{}{"enabled": false}) router := x.NewRouterPublic() publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin()) diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index ddcf26a20883..dd6ab755cf87 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -647,6 +647,8 @@ func TestRegistration(t *testing.T) { t.Run("method=PopulateSignUpMethod", func(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), + map[string]interface{}{"enabled": false}) conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/") testhelpers.SetDefaultIdentitySchema(conf, "file://stub/sort.schema.json") diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go index f5d332182163..a3e4b8f39dfb 100644 --- a/selfservice/strategy/webauthn/login_test.go +++ b/selfservice/strategy/webauthn/login_test.go @@ -75,6 +75,7 @@ var loginFixtureSuccessEmail = gjson.GetBytes(loginFixtureSuccessIdentity, "trai func TestCompleteLogin(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth)+".enabled", false) enableWebAuthn(conf) router := x.NewRouterPublic() diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index cc20d431230c..78e4a30050e4 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -49,6 +49,7 @@ func flowToIsSPA(flow string) bool { func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth)+".enabled", false) enableWebAuthn(conf) conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) diff --git a/session/manager_http_test.go b/session/manager_http_test.go index 8a1e166da25c..af33d8f73ed5 100644 --- a/session/manager_http_test.go +++ b/session/manager_http_test.go @@ -587,6 +587,9 @@ func TestDoesSessionSatisfy(t *testing.T) { t.Run(fmt.Sprintf("run=%d/desc=%s", k, tc.d), func(t *testing.T) { id := identity.NewIdentity("") for _, c := range tc.creds { + if c.Type == identity.CredentialsTypePassword { + id.Traits = []byte(`{"email":"` + c.Identifiers[0] + `"}`) + } id.SetCredentials(c.Type, c) } require.NoError(t, reg.IdentityManager().Create(context.Background(), id, identity.ManagerAllowWriteProtectedTraits)) diff --git a/text/id.go b/text/id.go index 562a179740ef..635b72367c6a 100644 --- a/text/id.go +++ b/text/id.go @@ -80,21 +80,22 @@ const ( ) const ( - InfoNodeLabel ID = 1070000 + iota // 1070000 - InfoNodeLabelInputPassword // 1070001 - InfoNodeLabelGenerated // 1070002 - InfoNodeLabelSave // 1070003 - InfoNodeLabelID // 1070004 - InfoNodeLabelSubmit // 1070005 - InfoNodeLabelVerifyOTP // 1070006 - InfoNodeLabelEmail // 1070007 - InfoNodeLabelResendOTP // 1070008 - InfoNodeLabelContinue // 1070009 - InfoNodeLabelRecoveryCode // 1070010 - InfoNodeLabelVerificationCode // 1070011 - InfoNodeLabelRegistrationCode // 1070012 - InfoNodeLabelLoginCode // 1070013 - InfoNodeLabelLoginAndLinkCredential + InfoNodeLabel ID = 1070000 + iota // 1070000 + InfoNodeLabelInputPassword // 1070001 + InfoNodeLabelGenerated // 1070002 + InfoNodeLabelSave // 1070003 + InfoNodeLabelID // 1070004 + InfoNodeLabelSubmit // 1070005 + InfoNodeLabelVerifyOTP // 1070006 + InfoNodeLabelEmail // 1070007 + InfoNodeLabelResendOTP // 1070008 + InfoNodeLabelContinue // 1070009 + InfoNodeLabelRecoveryCode // 1070010 + InfoNodeLabelVerificationCode // 1070011 + InfoNodeLabelRegistrationCode // 1070012 + InfoNodeLabelLoginCode // 1070013 + InfoNodeLabelLoginAndLinkCredential // 1070014 + InfoNodeLabelPhone // 1070015 ) const ( @@ -102,6 +103,7 @@ const ( InfoSelfServiceVerificationEmailSent // 1080001 InfoSelfServiceVerificationSuccessful // 1080002 InfoSelfServiceVerificationEmailWithCodeSent // 1080003 + InfoSelfServicePhoneVerificationSuccessful // 1080005 ) const ( @@ -142,6 +144,9 @@ const ( ErrorValidationPasswordTooManyBreaches ErrorValidationNoCodeUser ErrorValidationTraitsMismatch + ErrorValidationInvalidCode + ErrorValidationCodeSent + ErrorValidationCodeSMSSpam ) const ( diff --git a/text/message_node.go b/text/message_node.go index e2dfb7d6dc32..a66d8a0cb49c 100644 --- a/text/message_node.go +++ b/text/message_node.go @@ -117,3 +117,11 @@ func NewInfoNodeLoginAndLinkCredential() *Message { Type: Info, } } + +func NewInfoNodeInputPhone() *Message { + return &Message{ + ID: InfoNodeLabelPhone, + Text: "Phone", + Type: Info, + } +} diff --git a/text/message_validation.go b/text/message_validation.go index 28396180c0c5..72f9524dfd61 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -411,3 +411,27 @@ func NewErrorValidationTraitsMismatch() *Message { Type: Error, } } + +func NewErrorValidationInvalidCode() *Message { + return &Message{ + ID: ErrorValidationInvalidCode, + Text: "The provided code is invalid, check for spelling mistakes in your code or phone number.", + Type: Error, + } +} + +func NewErrorCodeSent() *Message { + return &Message{ + ID: ErrorValidationCodeSent, + Text: "Access code has been sent.", + Type: Error, + } +} + +func NewErrorValidationSMSSpam() *Message { + return &Message{ + ID: ErrorValidationCodeSMSSpam, + Text: "SMS spam detected.", + Type: Error, + } +} diff --git a/text/message_verification.go b/text/message_verification.go index 37064e2d0f6c..820a04ececd5 100644 --- a/text/message_verification.go +++ b/text/message_verification.go @@ -28,6 +28,14 @@ func NewInfoSelfServiceVerificationSuccessful() *Message { } } +func NewInfoSelfServicePhoneVerificationSuccessful() *Message { + return &Message{ + ID: InfoSelfServicePhoneVerificationSuccessful, + Type: Info, + Text: "You successfully verified your phone number.", + } +} + func NewVerificationEmailSent() *Message { return &Message{ ID: InfoSelfServiceVerificationEmailSent,