diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index 81d82247586b..48ecf4f581af 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -112,7 +112,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 5c2555eaddb2..036d598cc3a8 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -177,6 +177,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/sms/registration_code_valid.go b/courier/template/sms/registration_code_valid.go new file mode 100644 index 000000000000..3919aa499ef6 --- /dev/null +++ b/courier/template/sms/registration_code_valid.go @@ -0,0 +1,54 @@ +// 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 `json:"to"` + Traits map[string]interface{} `json:"traits"` + RegistrationCode string `json:"registration_code"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` + } +) + +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 4204df0ac4c8..f03185b2a835 100644 --- a/courier/template/sms/verification_code.go +++ b/courier/template/sms/verification_code.go @@ -19,6 +19,7 @@ type ( VerificationCodeValidModel struct { To string `json:"to"` + VerificationURL string `json:"verification_url"` VerificationCode string `json:"verification_code"` Identity map[string]interface{} `json:"identity"` RequestURL string `json:"request_url"` diff --git a/driver/config/config.go b/driver/config/config.go index 0d755a11ba63..80234e52f444 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -202,6 +202,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 ( @@ -317,6 +322,7 @@ type ( CourierWorkerPullCount(ctx context.Context) int CourierWorkerPullWait(ctx context.Context) time.Duration CourierChannels(context.Context) ([]*CourierChannel, error) + CourierTemplatesLoginValidSMS(ctx context.Context) string } ) @@ -1565,6 +1571,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 0b540ea25cd6..570bca2a2e5d 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1518,6 +1518,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/go.mod b/go.mod index 655a07bbf9e2..f19838d1b05f 100644 --- a/go.mod +++ b/go.mod @@ -253,7 +253,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect - github.com/nyaruka/phonenumbers v1.1.6 // indirect + github.com/nyaruka/phonenumbers v1.1.6 github.com/ogier/pflag v0.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect 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 9fbf7f08211d..1fba630193a9 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}) conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) testhelpers.SetDefaultIdentitySchemaFromRaw(conf, basicSchema) uiTS := testhelpers.NewRegistrationUIFlowEchoServer(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 813a0c8ad669..5025f1bd6cd8 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..42b095da65fc 100644 --- a/selfservice/flow/verification/fake_strategy.go +++ b/selfservice/flow/verification/fake_strategy.go @@ -31,7 +31,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) error { return nil } diff --git a/selfservice/flow/verification/strategy.go b/selfservice/flow/verification/strategy.go index 3d270bfb8732..96eedd76a652 100644 --- a/selfservice/flow/verification/strategy.go +++ b/selfservice/flow/verification/strategy.go @@ -29,7 +29,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) 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 6fdd039c146f..5108e085cd6d 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" @@ -40,7 +42,11 @@ type ( x.TracingProvider } Verifier struct { - r verifierDependencies + r verifierDependencies + dx *decoderx.HTTP + } + transientPayloadBody struct { + TransientPayload json.RawMessage `json:"transient_payload" form:"transient_payload"` } ) @@ -88,6 +94,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 @@ -141,7 +157,8 @@ func (e *Verifier) do( return err } - if err := strategy.SendVerificationEmail(ctx, verificationFlow, i, address); err != nil { + verificationFlow.TransientPayload = body.TransientPayload + if err := strategy.SendVerification(ctx, verificationFlow, i, address); err != nil { return err } diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 5e815fa4d4a4..da40b6f55427 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" @@ -29,12 +32,17 @@ import ( "github.com/ory/kratos/x" "github.com/ory/x/pointerx" "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&branding=brand-1")), + ) + assert.NoError(t, err) + u.Header.Set("Content-Type", "application/x-www-form-urlencoded") for _, tc := range []struct { name string @@ -216,6 +224,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, "transient_payload.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/verification.schema.json b/selfservice/strategy/code/.schema/verification.schema.json index e60989dcf52e..493ebc7f791f 100644 --- a/selfservice/strategy/code/.schema/verification.schema.json +++ b/selfservice/strategy/code/.schema/verification.schema.json @@ -17,6 +17,10 @@ "type": "string", "format": "email" }, + "phone": { + "type": "string", + "format": "phone" + }, "flow": { "type": "string", "format": "uuid" 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/TestRegistrationCode_SMS-case=registration-method=TestPopulateSignUpMethod.json b/selfservice/strategy/code/.snapshots/TestRegistrationCode_SMS-case=registration-method=TestPopulateSignUpMethod.json new file mode 100644 index 000000000000..e6fa56f4d574 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestRegistrationCode_SMS-case=registration-method=TestPopulateSignUpMethod.json @@ -0,0 +1,61 @@ +{ + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.email", + "type": "text", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "traits.phone", + "type": "text", + "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": 1040006, + "text": "Sign up with code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } + ] +} 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 fdc5e8b38b2d..080a65cf6a49 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -5,6 +5,7 @@ package code import ( "context" + "github.com/ory/x/randx" "net/url" "github.com/gofrs/uuid" @@ -101,12 +102,24 @@ 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, - RequestURL: f.GetRequestURL(), - TransientPayload: transientPayload, + var t courier.Template + switch address.Via { + case identity.ChannelTypeEmail: + t = email.NewRegistrationCodeValid(s.deps, &email.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Traits: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + }) + case identity.ChannelTypeSMS: + t = sms.NewRegistrationCodeValid(s.deps, &sms.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Traits: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, + }) } s.deps.Audit(). @@ -115,7 +128,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) } @@ -312,6 +325,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, @@ -375,6 +393,7 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo 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, RequestURL: f.GetRequestURL(), 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 fd8993447744..3b9bc30d8e7d 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 a9d7459f5c56..0ea4f6a1b906 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -7,6 +7,7 @@ import ( "context" "database/sql" "encoding/json" + "fmt" "net/http" "strings" @@ -195,6 +196,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) @@ -219,6 +222,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 @@ -237,9 +241,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, }} } @@ -248,14 +261,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, 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 @@ -318,12 +338,27 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi } } - 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) @@ -366,6 +401,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 19cac6d38375..e03ca722e5af 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" @@ -742,3 +747,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, "transient_payload.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 dca3054e0c8e..10a4dd503207 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" @@ -165,6 +167,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())) @@ -184,7 +188,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 } @@ -194,19 +198,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 + addresses := []Address{{ + To: cred.Identifiers[0], + Via: address.Via, + }} if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { return errors.WithStack(err) } // sets the flow state to code sent - f.SetState(flow.NextState(f.GetState())) + 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 27c645a94190..d653afcd6e54 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -8,12 +8,20 @@ import ( _ "embed" "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" "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" @@ -604,3 +612,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 2f80a4490982..d4d542530c00 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -6,6 +6,7 @@ package code import ( "context" "encoding/json" + "github.com/ory/x/randx" "net/http" "time" @@ -72,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 @@ -92,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"` @@ -154,6 +162,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()) @@ -195,8 +205,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")) } @@ -204,18 +214,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); 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) @@ -282,7 +306,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()). @@ -362,8 +390,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) (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, 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..0b71dc14ff80 --- /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, "transient_payload.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 6f25e324d8b2..26d93a8a66f5 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 6fe054f746fb..1148111f870d 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -317,7 +317,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) 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 cab8fec60b99..e0d01d63cbcf 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 14bf2382b212..54862bcc49af 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -650,6 +650,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/") conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) 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 035e7ffd984c..1df683cd5f8e 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -51,6 +51,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 a466caec0f8c..40a349751dbf 100644 --- a/text/id.go +++ b/text/id.go @@ -86,21 +86,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 ( @@ -108,6 +109,7 @@ const ( InfoSelfServiceVerificationEmailSent // 1080001 InfoSelfServiceVerificationSuccessful // 1080002 InfoSelfServiceVerificationEmailWithCodeSent // 1080003 + InfoSelfServicePhoneVerificationSuccessful // 1080005 ) const ( @@ -148,6 +150,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,