From 951485b55e1ab740f9f65e10eb80e5307b71ac45 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Thu, 13 Jul 2023 10:08:05 +0200 Subject: [PATCH] feat: login and registration with code style: format refactor: code cleanup and flow refinement feat: store code credential address type fix: login resend button and field errors fix: invalid code handling and error messages test(e2e): registration with code test: login and registration code fix: login and registration tests style: format test: registration with code error cases test: login with code error messages test: login with code test: login and registration code test: login errors fix: unit tests and verification flow fix: ui rendering on code group instead of default fix: sdk generation fix: sdk generation and tests chore: improve registration with code test chore: code review chore: code review chore: cleanup based on review comments --- .schema/openapi/patches/selfservice.yaml | 12 + cmd/clidoc/main.go | 18 +- courier/email_templates.go | 18 + courier/email_templates_test.go | 5 +- .../login_code/valid/email.body.gotmpl | 5 + .../valid/email.body.plaintext.gotmpl | 5 + .../login_code/valid/email.subject.gotmpl | 1 + .../registration_code/valid/email.body.gotmpl | 5 + .../valid/email.body.plaintext.gotmpl | 5 + .../valid/email.subject.gotmpl | 1 + courier/template/email/login_code_valid.go | 51 ++ .../template/email/login_code_valid_test.go | 30 + .../template/email/registration_code_valid.go | 51 ++ .../email/registration_code_valid_test.go | 30 + courier/template/template.go | 2 + courier/template/testhelpers/testhelpers.go | 7 +- driver/config/config.go | 55 +- driver/config/config_test.go | 20 +- driver/registry_default.go | 42 +- driver/registry_default_hooks.go | 7 + driver/registry_default_registration.go | 5 + driver/registry_default_test.go | 27 +- embedx/config.schema.json | 539 +++++------------- embedx/identity_extension.schema.json | 22 +- identity/credentials.go | 16 + identity/credentials_code.go | 27 +- identity/extension_credentials.go | 29 +- internal/client-go/.openapi-generator/FILES | 10 + internal/client-go/README.md | 5 + .../model_identity_credentials_otp.go | 162 ++++++ internal/client-go/model_login_flow.go | 34 +- internal/client-go/model_login_flow_state.go | 85 +++ internal/client-go/model_message.go | 2 +- internal/client-go/model_recovery_flow.go | 21 +- .../client-go/model_recovery_flow_state.go | 4 +- internal/client-go/model_registration_flow.go | 34 +- .../model_registration_flow_state.go | 85 +++ .../model_self_service_login_flow_state.go | 85 +++ .../model_self_service_recovery_flow_state.go | 13 +- ...el_self_service_registration_flow_state.go | 85 +++ .../model_self_service_settings_flow_state.go | 13 +- ...el_self_service_verification_flow_state.go | 13 +- internal/client-go/model_settings_flow.go | 21 +- .../client-go/model_settings_flow_state.go | 4 +- ...odel_update_login_flow_with_code_method.go | 249 ++++++++ ...date_registration_flow_with_code_method.go | 286 ++++++++++ internal/client-go/model_verification_flow.go | 21 +- .../model_verification_flow_state.go | 4 +- internal/httpclient/.openapi-generator/FILES | 10 + internal/httpclient/README.md | 5 + .../model_identity_credentials_otp.go | 162 ++++++ internal/httpclient/model_login_flow.go | 34 +- internal/httpclient/model_login_flow_state.go | 85 +++ internal/httpclient/model_message.go | 2 +- internal/httpclient/model_recovery_flow.go | 21 +- .../httpclient/model_recovery_flow_state.go | 4 +- .../httpclient/model_registration_flow.go | 34 +- .../model_registration_flow_state.go | 85 +++ internal/httpclient/model_settings_flow.go | 21 +- .../httpclient/model_settings_flow_state.go | 4 +- ...odel_update_login_flow_with_code_method.go | 249 ++++++++ ...date_registration_flow_with_code_method.go | 286 ++++++++++ .../httpclient/model_verification_flow.go | 21 +- .../model_verification_flow_state.go | 4 +- internal/testhelpers/courier.go | 27 +- persistence/reference.go | 2 + .../28ff0031-190b-4253-bd15-14308dec013e.json | 17 + .../bd292366-af32-4ba6-bdf0-11d6d1a217f3.json | 6 + .../00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json | 18 + .../0bc96cc9-dda4-4700-9e42-35731f2af91e.json | 3 +- .../1fb23c75-b809-42cc-8984-6ca2d0a1192f.json | 3 +- .../202c1981-1e25-47f0-8764-75ad506c2bec.json | 3 +- .../349c945a-60f8-436a-a301-7a42c92604f9.json | 3 +- .../38caf592-b042-4551-b92f-8d5223c2a4e2.json | 3 +- .../3a9ea34f-0f12-469b-9417-3ae5795a7baa.json | 3 +- .../43c99182-bb67-47e1-b564-bb23bd8d4393.json | 3 +- .../47edd3a8-0998-4779-9469-f4b8ee4430df.json | 3 +- .../56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json | 3 +- .../6d387820-f2f4-4f9f-9980-a90d89e7811f.json | 3 +- .../916ded11-aa64-4a27-b06e-96e221a509d7.json | 3 +- .../99974ce6-388c-4669-a95a-7757ee724020.json | 3 +- .../b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json | 3 +- .../cccccccc-dda4-4700-9e42-35731f2af911.json | 3 +- .../cccccccc-dda4-4700-9e42-35731f2af91e.json | 3 +- .../d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json | 3 +- .../f1f66a69-ce02-4a12-9591-9e02dda30a0d.json | 5 + .../05a7f09d-4ef3-41fb-958a-6ad74584b36a.json | 3 +- .../22d58184-b97d-44a5-bbaf-0aa8b4000d81.json | 3 +- .../2bf132e0-5d40-4df9-9a11-9106e5333735.json | 3 +- .../696e7022-c466-44f6-89c6-8cf93c06a62a.json | 3 +- .../69c80296-36cd-4afc-921a-15369cac5bf0.json | 14 + .../87fa3f43-5155-42b4-a1ad-174c2595fdaf.json | 3 +- .../8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json | 3 +- .../8f32efdc-f6fc-4c27-a3c2-579d109eff60.json | 3 +- .../9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json | 3 +- .../e2150cdc-23ac-4940-a240-6c79c27ab029.json | 3 +- .../ef18b06e-4700-4021-9949-ef783cd86be1.json | 3 +- .../ef18b06e-4700-4021-9949-ef783cd86be8.json | 3 +- .../f1b5ed18-113a-4a98-aae7-d4eba007199c.json | 3 +- persistence/sql/migratest/migration_test.go | 38 +- .../testdata/20230707133700_testdata.sql | 30 + .../testdata/20230707133701_testdata.sql | 23 + ...ce_registration_login_flows_state.down.sql | 2 + ...vice_registration_login_flows_state.up.sql | 2 + ...7133700000000_identity_login_code.down.sql | 4 + ...700000000_identity_login_code.mysql.up.sql | 30 + ...707133700000000_identity_login_code.up.sql | 29 + ...000001_identity_registration_code.down.sql | 4 + ...01_identity_registration_code.mysql.up.sql | 28 + ...00000001_identity_registration_code.up.sql | 28 + ...73852000000_credential_types_code.down.sql | 1 + ...2173852000000_credential_types_code.up.sql | 1 + persistence/sql/persister_login.go | 124 ++++ persistence/sql/persister_recovery.go | 13 +- persistence/sql/persister_registration.go | 136 +++++ persistence/sql/persister_verification.go | 2 +- schema/errors.go | 40 ++ schema/extension.go | 4 + selfservice/flow/error_test.go | 23 + selfservice/flow/flow.go | 3 + selfservice/flow/login/flow.go | 24 + selfservice/flow/login/hook.go | 1 - selfservice/flow/login/sort.go | 1 + selfservice/flow/login/state.go | 17 + selfservice/flow/name.go | 28 + selfservice/flow/recovery/flow.go | 16 +- selfservice/flow/recovery/flow_test.go | 2 +- selfservice/flow/recovery/handler.go | 1 - selfservice/flow/recovery/state.go | 33 +- selfservice/flow/registration/flow.go | 23 + selfservice/flow/registration/sort.go | 1 + selfservice/flow/registration/state.go | 15 + selfservice/flow/request.go | 30 +- selfservice/flow/request_test.go | 74 ++- selfservice/flow/settings/flow.go | 16 +- selfservice/flow/settings/hook.go | 2 +- selfservice/flow/settings/state.go | 9 +- selfservice/flow/state.go | 49 ++ selfservice/flow/{recovery => }/state_test.go | 2 +- selfservice/flow/verification/error.go | 4 +- selfservice/flow/verification/flow.go | 16 +- selfservice/flow/verification/flow_test.go | 3 +- selfservice/flow/verification/state.go | 33 +- selfservice/flow/verification/state_test.go | 20 - selfservice/hook/code_address_verifier.go | 65 +++ .../hook/code_address_verifier_test.go | 100 ++++ selfservice/hook/stub/code.schema.json | 27 + selfservice/hook/verification.go | 8 +- selfservice/hook/verification_test.go | 5 +- .../strategy/code/.schema/login.schema.json | 9 +- .../code/.schema/registration.schema.json | 32 ++ ...erification_payloads_after_submission.json | 26 +- selfservice/strategy/code/code_login.go | 87 ++- .../strategy/code/code_registration.go | 96 ++++ selfservice/strategy/code/code_sender.go | 94 +++ selfservice/strategy/code/code_sender_test.go | 4 - selfservice/strategy/code/persistence.go | 22 + selfservice/strategy/code/schema.go | 3 + selfservice/strategy/code/strategy.go | 353 +++++++++++- selfservice/strategy/code/strategy_login.go | 251 ++++++-- .../strategy/code/strategy_login_test.go | 353 ++++++++++++ .../strategy/code/strategy_recovery.go | 52 +- .../strategy/code/strategy_recovery_test.go | 18 +- .../strategy/code/strategy_registration.go | 267 +++++++++ .../code/strategy_registration_test.go | 445 +++++++++++++++ .../strategy/code/strategy_verification.go | 45 +- .../code/strategy_verification_test.go | 59 +- .../code/stub/code.identity.schema.json | 61 ++ selfservice/strategy/link/strategy.go | 22 +- .../strategy/link/strategy_recovery.go | 20 +- .../strategy/link/strategy_recovery_test.go | 53 +- .../strategy/link/strategy_verification.go | 19 +- .../link/strategy_verification_test.go | 46 +- selfservice/strategy/lookup/login.go | 2 +- selfservice/strategy/lookup/settings.go | 4 +- selfservice/strategy/lookup/settings_test.go | 18 +- selfservice/strategy/oidc/strategy.go | 3 + selfservice/strategy/oidc/strategy_login.go | 4 +- .../strategy/oidc/strategy_registration.go | 4 +- .../strategy/oidc/strategy_settings_test.go | 98 ++-- selfservice/strategy/password/login.go | 2 +- selfservice/strategy/password/registration.go | 2 +- selfservice/strategy/password/settings.go | 4 +- selfservice/strategy/password/strategy.go | 8 +- selfservice/strategy/profile/strategy.go | 4 +- selfservice/strategy/profile/strategy_test.go | 51 +- selfservice/strategy/totp/login.go | 2 +- selfservice/strategy/totp/settings.go | 6 +- selfservice/strategy/totp/settings_test.go | 18 +- ...ebauthn_login_is_invalid-type=browser.json | 3 +- ...if_webauthn_login_is_invalid-type=spa.json | 3 +- selfservice/strategy/webauthn/login.go | 2 +- selfservice/strategy/webauthn/registration.go | 2 +- selfservice/strategy/webauthn/settings.go | 4 +- .../strategy/webauthn/settings_test.go | 10 +- spec/api.json | 178 ++++-- spec/swagger.json | 154 ++++- test/e2e/.go-version | 1 + .../profiles/code/login/error.spec.ts | 189 ++++++ .../profiles/code/registration/error.spec.ts | 161 ++++++ .../code/registration/success.spec.ts | 279 +++++++++ test/e2e/cypress/support/commands.ts | 168 +++++- test/e2e/cypress/support/config.d.ts | 131 +++-- test/e2e/cypress/support/index.d.ts | 55 +- test/e2e/profiles/code/.kratos.yml | 40 ++ .../identity.code.only.traits.schema.json | 31 + .../code/identity.complex.traits.schema.json | 69 +++ .../profiles/code/identity.traits.schema.json | 34 ++ test/e2e/run.sh | 44 +- text/id.go | 40 +- text/message_login.go | 35 ++ text/message_node.go | 16 + text/message_registration.go | 35 ++ text/message_validation.go | 18 + ui/container/container.go | 3 +- x/xsql/sql.go | 2 + 216 files changed, 7818 insertions(+), 1217 deletions(-) create mode 100644 courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl create mode 100644 courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl create mode 100644 courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl create mode 100644 courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl create mode 100644 courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl create mode 100644 courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl create mode 100644 courier/template/email/login_code_valid.go create mode 100644 courier/template/email/login_code_valid_test.go create mode 100644 courier/template/email/registration_code_valid.go create mode 100644 courier/template/email/registration_code_valid_test.go create mode 100644 internal/client-go/model_identity_credentials_otp.go create mode 100644 internal/client-go/model_login_flow_state.go create mode 100644 internal/client-go/model_registration_flow_state.go create mode 100644 internal/client-go/model_self_service_login_flow_state.go create mode 100644 internal/client-go/model_self_service_registration_flow_state.go create mode 100644 internal/client-go/model_update_login_flow_with_code_method.go create mode 100644 internal/client-go/model_update_registration_flow_with_code_method.go create mode 100644 internal/httpclient/model_identity_credentials_otp.go create mode 100644 internal/httpclient/model_login_flow_state.go create mode 100644 internal/httpclient/model_registration_flow_state.go create mode 100644 internal/httpclient/model_update_login_flow_with_code_method.go create mode 100644 internal/httpclient/model_update_registration_flow_with_code_method.go create mode 100644 persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json create mode 100644 persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json create mode 100644 persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json create mode 100644 persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json create mode 100644 persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json create mode 100644 persistence/sql/migratest/testdata/20230707133700_testdata.sql create mode 100644 persistence/sql/migratest/testdata/20230707133701_testdata.sql create mode 100644 persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql create mode 100644 persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql create mode 100644 persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql create mode 100644 persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql create mode 100644 persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql create mode 100644 persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql create mode 100644 persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql create mode 100644 persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql create mode 100644 persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql create mode 100644 persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql create mode 100644 selfservice/flow/login/state.go create mode 100644 selfservice/flow/name.go create mode 100644 selfservice/flow/registration/state.go create mode 100644 selfservice/flow/state.go rename selfservice/flow/{recovery => }/state_test.go (97%) delete mode 100644 selfservice/flow/verification/state_test.go create mode 100644 selfservice/hook/code_address_verifier.go create mode 100644 selfservice/hook/code_address_verifier_test.go create mode 100644 selfservice/hook/stub/code.schema.json create mode 100644 selfservice/strategy/code/.schema/registration.schema.json create mode 100644 selfservice/strategy/code/code_registration.go create mode 100644 selfservice/strategy/code/strategy_login_test.go create mode 100644 selfservice/strategy/code/strategy_registration.go create mode 100644 selfservice/strategy/code/strategy_registration_test.go create mode 100644 selfservice/strategy/code/stub/code.identity.schema.json mode change 100755 => 100644 spec/api.json create mode 100644 test/e2e/.go-version create mode 100644 test/e2e/cypress/integration/profiles/code/login/error.spec.ts create mode 100644 test/e2e/cypress/integration/profiles/code/registration/error.spec.ts create mode 100644 test/e2e/cypress/integration/profiles/code/registration/success.spec.ts create mode 100644 test/e2e/profiles/code/.kratos.yml create mode 100644 test/e2e/profiles/code/identity.code.only.traits.schema.json create mode 100644 test/e2e/profiles/code/identity.complex.traits.schema.json create mode 100644 test/e2e/profiles/code/identity.traits.schema.json diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index aba0dde128b0..102c1fe60deb 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -25,6 +25,12 @@ password: "#/components/schemas/updateRegistrationFlowWithPasswordMethod" oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" +- op: add + path: /components/schemas/registrationFlowState/enum + value: + - choose_method + - sent_email + - passed_challenge # end # All modifications for the login flow @@ -48,6 +54,12 @@ totp: "#/components/schemas/updateLoginFlowWithTotpMethod" webauthn: "#/components/schemas/updateLoginFlowWithWebAuthnMethod" lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod" +- op: add + path: /components/schemas/loginFlowState/enum + value: + - choose_method + - sent_email + - passed_challenge # end # All modifications for the recovery flow diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go index 6b4bbd7840af..a899a0a35045 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -27,8 +27,10 @@ import ( "github.com/ory/x/clidoc" ) -var aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) -var inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) +var ( + aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) + inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) +) var messages map[string]*text.Message @@ -145,6 +147,18 @@ func init() { "NewInfoSelfServiceContinueLoginWebAuthn": text.NewInfoSelfServiceContinueLoginWebAuthn(), "NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(), "NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(), + "NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(), + "NewLoginEmailWithCodeSent": text.NewLoginEmailWithCodeSent(), + "NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed": text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed(), + "NewErrorValidationLoginCodeInvalidOrAlreadyUsed": text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed(), + "NewErrorValidationNoCodeUser": text.NewErrorValidationNoCodeUser(), + "NewInfoNodeLabelRegistrationCode": text.NewInfoNodeLabelRegistrationCode(), + "NewInfoNodeLabelLoginCode": text.NewInfoNodeLabelLoginCode(), + "NewErrorValidationLoginRetrySuccessful": text.NewErrorValidationLoginRetrySuccessful(), + "NewErrorValidationTraitsMismatch": text.NewErrorValidationTraitsMismatch(), + "NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(), + "NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(), + "NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(), } } diff --git a/courier/email_templates.go b/courier/email_templates.go index 8d5d51f0ceaa..d2bae0a197e4 100644 --- a/courier/email_templates.go +++ b/courier/email_templates.go @@ -40,6 +40,8 @@ const ( TypeVerificationCodeValid TemplateType = "verification_code_valid" TypeOTP TemplateType = "otp" TypeTestStub TemplateType = "stub" + TypeLoginCodeValid TemplateType = "login_code_valid" + TypeRegistrationCodeValid TemplateType = "registration_code_valid" ) func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) { @@ -60,6 +62,10 @@ func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) { return TypeVerificationCodeInvalid, nil case *email.VerificationCodeValid: return TypeVerificationCodeValid, nil + case *email.LoginCodeValid: + return TypeLoginCodeValid, nil + case *email.RegistrationCodeValid: + return TypeRegistrationCodeValid, nil case *email.TestStub: return TypeTestStub, nil default: @@ -123,6 +129,18 @@ func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTem return nil, err } return email.NewTestStub(d, &t), nil + case TypeLoginCodeValid: + var t email.LoginCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewLoginCodeValid(d, &t), nil + case TypeRegistrationCodeValid: + var t email.RegistrationCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRegistrationCodeValid(d, &t), nil default: return nil, errors.Errorf("received unexpected message template type: %s", msg.TemplateType) } diff --git a/courier/email_templates_test.go b/courier/email_templates_test.go index 2e8f8520bb7f..40afb5dc6863 100644 --- a/courier/email_templates_test.go +++ b/courier/email_templates_test.go @@ -27,6 +27,8 @@ func TestGetTemplateType(t *testing.T) { courier.TypeVerificationCodeInvalid: &email.VerificationCodeInvalid{}, courier.TypeVerificationCodeValid: &email.VerificationCodeValid{}, courier.TypeTestStub: &email.TestStub{}, + courier.TypeLoginCodeValid: &email.LoginCodeValid{}, + courier.TypeRegistrationCodeValid: &email.RegistrationCodeValid{}, } { t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) { actualType, err := courier.GetEmailTemplateType(tmpl) @@ -50,6 +52,8 @@ func TestNewEmailTemplateFromMessage(t *testing.T) { courier.TypeVerificationCodeInvalid: email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{To: "baz"}), courier.TypeVerificationCodeValid: email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{To: "faz", VerificationURL: "http://bar.foo", VerificationCode: "123456678"}), courier.TypeTestStub: email.NewTestStub(reg, &email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), + courier.TypeLoginCodeValid: email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{To: "far", LoginCode: "123456"}), + courier.TypeRegistrationCodeValid: email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{To: "far", RegistrationCode: "123456"}), } { t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { tmplData, err := json.Marshal(expectedTmpl) @@ -84,7 +88,6 @@ func TestNewEmailTemplateFromMessage(t *testing.T) { actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext(ctx) require.NoError(t, err) require.Equal(t, expectedBodyPlaintext, actualBodyPlaintext) - }) } } diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl new file mode 100644 index 000000000000..505684b9849b --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please login to your account by entering the following code: + +{{ .LoginCode }} diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 000000000000..505684b9849b --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please login to your account by entering the following code: + +{{ .LoginCode }} diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl new file mode 100644 index 000000000000..19d7bfd57d49 --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Login to your account diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl new file mode 100644 index 000000000000..6b9c31799995 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please complete your account registration by entering the following code: + +{{ .RegistrationCode }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 000000000000..6b9c31799995 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please complete your account registration by entering the following code: + +{{ .RegistrationCode }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl new file mode 100644 index 000000000000..0f36292619ef --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Complete your account registration diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go new file mode 100644 index 000000000000..2debc3a0cb7c --- /dev/null +++ b/courier/template/email/login_code_valid.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + LoginCodeValid struct { + deps template.Dependencies + model *LoginCodeValidModel + } + LoginCodeValidModel struct { + To string + LoginCode string + Identity map[string]interface{} + } +) + +func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid { + return &LoginCodeValid{deps: d, model: m} +} + +func (t *LoginCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *LoginCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.subject.gotmpl", "login_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *LoginCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.gotmpl", "login_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.HTML) +} + +func (t *LoginCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.plaintext.gotmpl", "login_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.PlainText) +} + +func (t *LoginCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} diff --git a/courier/template/email/login_code_valid_test.go b/courier/template/email/login_code_valid_test.go new file mode 100644 index 000000000000..dca97defe08c --- /dev/null +++ b/courier/template/email/login_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/internal" +) + +func TestLoginCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/login_code/valid", courier.TypeLoginCodeValid) + }) +} diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go new file mode 100644 index 000000000000..f7e39e334976 --- /dev/null +++ b/courier/template/email/registration_code_valid.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RegistrationCodeValid struct { + deps template.Dependencies + model *RegistrationCodeValidModel + } + RegistrationCodeValidModel struct { + To string + Traits map[string]interface{} + RegistrationCode string + } +) + +func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid { + return &RegistrationCodeValid{deps: d, model: m} +} + +func (t *RegistrationCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *RegistrationCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.subject.gotmpl", "registration_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *RegistrationCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.gotmpl", "registration_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.HTML) +} + +func (t *RegistrationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.plaintext.gotmpl", "registration_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.PlainText) +} + +func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} diff --git a/courier/template/email/registration_code_valid_test.go b/courier/template/email/registration_code_valid_test.go new file mode 100644 index 000000000000..be4cfe8059ea --- /dev/null +++ b/courier/template/email/registration_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/internal" +) + +func TestRegistrationCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/registration_code/valid", courier.TypeRegistrationCodeValid) + }) +} diff --git a/courier/template/template.go b/courier/template/template.go index 3ee99428aa5b..483c40bd2f5e 100644 --- a/courier/template/template.go +++ b/courier/template/template.go @@ -19,6 +19,8 @@ type ( CourierTemplatesVerificationValid() *config.CourierEmailTemplate CourierTemplatesRecoveryInvalid() *config.CourierEmailTemplate CourierTemplatesRecoveryValid() *config.CourierEmailTemplate + CourierTemplatesLoginValid() *config.CourierEmailTemplate + CourierTemplatesRegistrationValid() *config.CourierEmailTemplate } Dependencies interface { diff --git a/courier/template/testhelpers/testhelpers.go b/courier/template/testhelpers/testhelpers.go index 936eedb0a65e..6c2923dbe416 100644 --- a/courier/template/testhelpers/testhelpers.go +++ b/courier/template/testhelpers/testhelpers.go @@ -40,7 +40,8 @@ func SetupRemoteConfig(t *testing.T, ctx context.Context, plaintext string, html func TestRendered(t *testing.T, ctx context.Context, tpl interface { EmailBody(context.Context) (string, error) EmailSubject(context.Context) (string, error) -}) { +}, +) { rendered, err := tpl.EmailBody(ctx) require.NoError(t, err) assert.NotEmpty(t, rendered) @@ -83,6 +84,10 @@ func TestRemoteTemplates(t *testing.T, basePath string, tmplType courier.Templat return email.NewVerificationCodeInvalid(d, &email.VerificationCodeInvalidModel{}) case courier.TypeVerificationCodeValid: return email.NewVerificationCodeValid(d, &email.VerificationCodeValidModel{}) + case courier.TypeLoginCodeValid: + return email.NewLoginCodeValid(d, &email.LoginCodeValidModel{}) + case courier.TypeRegistrationCodeValid: + return email.NewRegistrationCodeValid(d, &email.RegistrationCodeValidModel{}) default: return nil } diff --git a/driver/config/config.go b/driver/config/config.go index 1a0b76c6eb90..f0e585f9d744 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -68,6 +68,8 @@ const ( ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email" ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy" ViperKeyCourierHTTPRequestConfig = "courier.http.request_config" + ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email" + ViperKeyCourierTemplatesRegistrationCodeValidEmail = "courier.templates.registration_code.valid.email" ViperKeyCourierSMTPFrom = "courier.smtp.from_address" ViperKeyCourierSMTPFromName = "courier.smtp.from_name" ViperKeyCourierSMTPHeaders = "courier.smtp.headers" @@ -226,6 +228,11 @@ type ( Enabled bool `json:"enabled"` Config json.RawMessage `json:"config"` } + SelfServiceStrategyCode struct { + RegistrationEnabled bool `json:"registration_enabled"` + LoginEnabled bool `json:"login_enabled"` + *SelfServiceStrategy + } Schema struct { ID string `json:"id" koanf:"id"` URL string `json:"url" koanf:"url"` @@ -279,6 +286,8 @@ type ( CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate + CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate CourierMessageRetries(ctx context.Context) int } ) @@ -729,7 +738,8 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self config = c } - enabledKey := fmt.Sprintf("%s.%s.enabled", ViperKeySelfServiceStrategyConfig, strategy) + basePath := fmt.Sprintf("%s.%s", ViperKeySelfServiceStrategyConfig, strategy) + enabledKey := fmt.Sprintf("%s.enabled", basePath) s := &SelfServiceStrategy{ Enabled: pp.Bool(enabledKey), Config: json.RawMessage(config), @@ -739,6 +749,7 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self // we need to forcibly set these values here: if !pp.Exists(enabledKey) { switch strategy { + case "otp": case "password": fallthrough case "profile": @@ -755,6 +766,40 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self return s } +func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrategyCode { + pp := p.GetProvider(ctx) + + config := "{}" + out, err := pp.Marshal(kjson.Parser()) + if err != nil { + p.l.WithError(err).Warn("Unable to marshal self service strategy configuration.") + } else if c := gjson.GetBytes(out, + fmt.Sprintf("%s.%s.config", ViperKeySelfServiceStrategyConfig, "code")).Raw; len(c) > 0 { + config = c + } + + basePath := fmt.Sprintf("%s.%s", ViperKeySelfServiceStrategyConfig, "code") + enabledKey := fmt.Sprintf("%s.enabled", basePath) + registrationKey := fmt.Sprintf("%s.registration_enabled", basePath) + loginKey := fmt.Sprintf("%s.login_enabled", basePath) + + s := &SelfServiceStrategyCode{ + SelfServiceStrategy: &SelfServiceStrategy{ + Enabled: pp.Bool(enabledKey), + Config: json.RawMessage(config), + }, + RegistrationEnabled: pp.Bool(registrationKey), + LoginEnabled: pp.Bool(loginKey), + } + + if !pp.Exists(enabledKey) { + s.RegistrationEnabled = false + s.LoginEnabled = false + s.Enabled = true + } + return s +} + func (p *Config) SecretsDefault(ctx context.Context) [][]byte { pp := p.GetProvider(ctx) secrets := pp.Strings(ViperKeySecretsDefault) @@ -1096,6 +1141,14 @@ func (p *Config) CourierTemplatesVerificationCodeValid(ctx context.Context) *Cou return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidEmail) } +func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail) +} + +func (p *Config) CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate { + return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidEmail) +} + func (p *Config) CourierMessageRetries(ctx context.Context) int { return p.GetProvider(ctx).IntF(ViperKeyCourierMessageRetries, 5) } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 13a16d7e48fe..19a9dbaf7d10 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -381,8 +381,10 @@ func TestViperProvider(t *testing.T) { t.Run("group=hashers", func(t *testing.T) { c := p.HasherArgon2(ctx) - assert.Equal(t, &config.Argon2{Memory: 1048576, Iterations: 2, Parallelism: 4, - SaltLength: 16, KeyLength: 32, DedicatedMemory: config.Argon2DefaultDedicatedMemory, ExpectedDeviation: config.Argon2DefaultDeviation, ExpectedDuration: config.Argon2DefaultDuration}, c) + assert.Equal(t, &config.Argon2{ + Memory: 1048576, Iterations: 2, Parallelism: 4, + SaltLength: 16, KeyLength: 32, DedicatedMemory: config.Argon2DefaultDedicatedMemory, ExpectedDeviation: config.Argon2DefaultDeviation, ExpectedDuration: config.Argon2DefaultDuration, + }, c) }) t.Run("group=set_provider_by_json", func(t *testing.T) { @@ -505,6 +507,8 @@ func TestViperProvider_Defaults(t *testing.T) { assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) }, }, @@ -520,6 +524,8 @@ func TestViperProvider_Defaults(t *testing.T) { assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) }, }, @@ -535,6 +541,8 @@ func TestViperProvider_Defaults(t *testing.T) { assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) }, }, { @@ -561,6 +569,8 @@ func TestViperProvider_Defaults(t *testing.T) { assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) assert.False(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)) assert.False(t, p.SelfServiceFlowVerificationNotifyUnknownRecipients(ctx)) @@ -897,7 +907,6 @@ func TestLoadingTLSConfig(t *testing.T) { assert.Equal(t, "Unable to load HTTPS TLS Certificate", hook.LastEntry().Message) assert.True(t, *exited) }) - } func TestIdentitySchemaValidation(t *testing.T) { @@ -1022,7 +1031,6 @@ func TestIdentitySchemaValidation(t *testing.T) { assert.Error(t, e) assert.Contains(t, e.Error(), "Client.Timeout") } - }) t.Run("case=validate schema is validated on file change", func(t *testing.T) { @@ -1051,7 +1059,7 @@ func TestIdentitySchemaValidation(t *testing.T) { // There are a bunch of log messages beeing logged. We are looking for a specific one. timeout := time.After(time.Millisecond * 500) - var success = false + success := false for !success { for _, v := range hook.AllEntries() { s, err := v.String() @@ -1064,7 +1072,7 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Fatal("the test could not complete as the context timed out before the file watcher updated") case <-timeout: t.Fatal("Expected log line was not encountered within specified timeout") - default: //nothing + default: // nothing } } diff --git a/driver/registry_default.go b/driver/registry_default.go index f1307e310902..b5183464a75e 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -96,11 +96,12 @@ type RegistryDefault struct { persister persistence.Persister migrationStatus popx.MigrationStatuses - hookVerifier *hook.Verifier - hookSessionIssuer *hook.SessionIssuer - hookSessionDestroyer *hook.SessionDestroyer - hookAddressVerifier *hook.AddressVerifier - hookShowVerificationUI *hook.ShowVerificationUIHook + hookVerifier *hook.Verifier + hookSessionIssuer *hook.SessionIssuer + hookSessionDestroyer *hook.SessionDestroyer + hookAddressVerifier *hook.AddressVerifier + hookShowVerificationUI *hook.ShowVerificationUIHook + hookCodeAddressVerifier *hook.CodeAddressVerifier identityHandler *identity.Handler identityValidator *identity.Validator @@ -326,10 +327,28 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} { return m.selfserviceStrategies } +func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool { + switch id { + case identity.CredentialsTypeCodeAuth.String(): + return m.Config().SelfServiceCodeStrategy(ctx).RegistrationEnabled + default: + return m.Config().SelfServiceStrategy(ctx, id).Enabled + } +} + +func (m *RegistryDefault) strategyLoginEnabled(ctx context.Context, id string) bool { + switch id { + case identity.CredentialsTypeCodeAuth.String(): + return m.Config().SelfServiceCodeStrategy(ctx).LoginEnabled + default: + return m.Config().SelfServiceStrategy(ctx, id).Enabled + } +} + func (m *RegistryDefault) RegistrationStrategies(ctx context.Context) (registrationStrategies registration.Strategies) { for _, strategy := range m.selfServiceStrategies() { if s, ok := strategy.(registration.Strategy); ok { - if m.Config().SelfServiceStrategy(ctx, string(s.ID())).Enabled { + if m.strategyRegistrationEnabled(ctx, s.ID().String()) { registrationStrategies = append(registrationStrategies, s) } } @@ -351,7 +370,7 @@ func (m *RegistryDefault) AllRegistrationStrategies() registration.Strategies { func (m *RegistryDefault) LoginStrategies(ctx context.Context) (loginStrategies login.Strategies) { for _, strategy := range m.selfServiceStrategies() { if s, ok := strategy.(login.Strategy); ok { - if m.Config().SelfServiceStrategy(ctx, string(s.ID())).Enabled { + if m.strategyLoginEnabled(ctx, s.ID().String()) { loginStrategies = append(loginStrategies, s) } } @@ -649,7 +668,6 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize m.persister = p.WithNetworkID(net.ID) return nil }, bc) - if err != nil { return err } @@ -733,6 +751,14 @@ func (m *RegistryDefault) VerificationCodePersister() code.VerificationCodePersi return m.Persister() } +func (m *RegistryDefault) RegistrationCodePersister() code.RegistrationCodePersister { + return m.Persister() +} + +func (m *RegistryDefault) LoginCodePersister() code.LoginCodePersister { + return m.Persister() +} + func (m *RegistryDefault) Persister() persistence.Persister { return m.persister } diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index 6efffc05a777..c3f809d2144e 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -15,6 +15,13 @@ func (m *RegistryDefault) HookVerifier() *hook.Verifier { return m.hookVerifier } +func (m *RegistryDefault) HookCodeAddressVerifier() *hook.CodeAddressVerifier { + if m.hookCodeAddressVerifier == nil { + m.hookCodeAddressVerifier = hook.NewCodeAddressVerifier(m) + } + return m.hookCodeAddressVerifier +} + func (m *RegistryDefault) HookSessionIssuer() *hook.SessionIssuer { if m.hookSessionIssuer == nil { m.hookSessionIssuer = hook.NewSessionIssuer(m) diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 7f78517891f0..060afcbdf5c7 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -28,6 +28,11 @@ func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context, initialHookCount = 1 } + if credentialsType == identity.CredentialsTypeCodeAuth && m.Config().SelfServiceCodeStrategy(ctx).RegistrationEnabled { + b = append(b, m.HookCodeAddressVerifier()) + initialHookCount += 1 + } + for _, v := range m.getHooks(string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType))) { if hook, ok := v.(registration.PostHookPostPersistExecutor); ok { b = append(b, hook) diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 3bccf3e24b03..3a4be63a768c 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -627,7 +627,8 @@ func TestDriverDefault_Strategies(t *testing.T) { { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - }}, + }, + }, { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) @@ -649,6 +650,13 @@ func TestDriverDefault_Strategies(t *testing.T) { }, expect: []string{"password", "oidc"}, }, + { + prep: func(conf *config.Config) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.registration_enabled", true) + }, + expect: []string{"password", "code"}, + }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { conf, reg := internal.NewVeryFastRegistryWithoutDB(t) @@ -672,7 +680,8 @@ func TestDriverDefault_Strategies(t *testing.T) { { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - }}, + }, + }, { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) @@ -694,6 +703,13 @@ func TestDriverDefault_Strategies(t *testing.T) { }, expect: []string{"password", "oidc", "totp"}, }, + { + prep: func(conf *config.Config) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.login_enabled", true) + }, + expect: []string{"password", "code"}, + }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { conf, reg := internal.NewVeryFastRegistryWithoutDB(t) @@ -760,7 +776,8 @@ func TestDriverDefault_Strategies(t *testing.T) { }), configx.SkipValidation()) return c - }}, + }, + }, { prep: func(t *testing.T) *config.Config { c := config.MustNew(t, l, @@ -834,7 +851,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("case=all login strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "totp", "webauthn", "lookup_secret"} + expects := []string{"password", "oidc", "code", "totp", "webauthn", "lookup_secret"} s := reg.AllLoginStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -843,7 +860,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { }) t.Run("case=all registration strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "webauthn"} + expects := []string{"password", "oidc", "code", "webauthn"} s := reg.AllRegistrationStrategies() require.Len(t, s, len(expects)) for k, e := range expects { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 61ba37c6708f..0bcb38e2a902 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -43,10 +43,7 @@ "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/dashboard", - "/dashboard" - ] + "examples": ["https://my-app.com/dashboard", "/dashboard"] }, "selfServiceSessionRevokerHook": { "type": "object", @@ -56,9 +53,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceSessionIssuerHook": { "type": "object", @@ -68,9 +63,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceRequireVerifiedAddressHook": { "type": "object", @@ -80,9 +73,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceShowVerificationUIHook": { "type": "object", @@ -92,9 +83,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "webHookAuthBasicAuthProperties": { "properties": { @@ -114,17 +103,11 @@ } }, "additionalProperties": false, - "required": [ - "user", - "password" - ] + "required": ["user", "password"] } }, "additionalProperties": false, - "required": [ - "type", - "config" - ] + "required": ["type", "config"] }, "httpRequestConfig": { "type": "object", @@ -132,9 +115,7 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to send the emails to.", - "examples": [ - "https://example.com/api/v1/email" - ], + "examples": ["https://example.com/api/v1/email"], "type": "string", "pattern": "^https?://" }, @@ -199,25 +180,15 @@ "in": { "type": "string", "description": "How the api key should be transferred", - "enum": [ - "header", - "cookie" - ] + "enum": ["header", "cookie"] } }, "additionalProperties": false, - "required": [ - "name", - "value", - "in" - ] + "required": ["name", "value", "in"] } }, "additionalProperties": false, - "required": [ - "type", - "config" - ] + "required": ["type", "config"] }, "selfServiceWebHook": { "type": "object", @@ -256,10 +227,7 @@ "const": true } }, - "required": [ - "ignore", - "parse" - ] + "required": ["ignore", "parse"] } }, "url": { @@ -320,46 +288,30 @@ "response": { "properties": { "ignore": { - "enum": [ - true - ] + "enum": [true] } }, - "required": [ - "ignore" - ] + "required": ["ignore"] } }, - "required": [ - "response" - ] + "required": ["response"] } }, { "properties": { "can_interrupt": { - "enum": [ - false - ] + "enum": [false] } }, - "require": [ - "can_interrupt" - ] + "require": ["can_interrupt"] } ], "additionalProperties": false, - "required": [ - "url", - "method" - ] + "required": ["url", "method"] } }, "additionalProperties": false, - "required": [ - "hook", - "config" - ] + "required": ["hook", "config"] }, "OIDCClaims": { "title": "OpenID Connect claims", @@ -392,9 +344,7 @@ "essential": true }, "acr": { - "values": [ - "urn:mace:incommon:iap:silver" - ] + "values": ["urn:mace:incommon:iap:silver"] } } } @@ -442,9 +392,7 @@ "properties": { "id": { "type": "string", - "examples": [ - "google" - ] + "examples": ["google"] }, "provider": { "title": "Provider", @@ -471,9 +419,7 @@ "linkedin", "lark" ], - "examples": [ - "google" - ] + "examples": ["google"] }, "label": { "title": "Optional string which will be used when generating labels for UI buttons.", @@ -488,23 +434,17 @@ "issuer_url": { "type": "string", "format": "uri", - "examples": [ - "https://accounts.google.com" - ] + "examples": ["https://accounts.google.com"] }, "auth_url": { "type": "string", "format": "uri", - "examples": [ - "https://accounts.google.com/o/oauth2/v2/auth" - ] + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] }, "token_url": { "type": "string", "format": "uri", - "examples": [ - "https://www.googleapis.com/oauth2/v4/token" - ] + "examples": ["https://www.googleapis.com/oauth2/v4/token"] }, "mapper_url": { "title": "Jsonnet Mapper URL", @@ -521,10 +461,7 @@ "type": "array", "items": { "type": "string", - "examples": [ - "offline_access", - "profile" - ] + "examples": ["offline_access", "profile"] } }, "microsoft_tenant": { @@ -543,30 +480,21 @@ "title": "Microsoft subject source", "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier.", "type": "string", - "enum": [ - "userinfo", - "me" - ], + "enum": ["userinfo", "me"], "default": "userinfo", - "examples": [ - "userinfo" - ] + "examples": ["userinfo"] }, "apple_team_id": { "title": "Apple Developer Team ID", "description": "Apple Developer Team ID needed for generating a JWT token for client secret", "type": "string", - "examples": [ - "KP76DQS54M" - ] + "examples": ["KP76DQS54M"] }, "apple_private_key_id": { "title": "Apple Private Key Identifier", "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", "type": "string", - "examples": [ - "UX56C66723" - ] + "examples": ["UX56C66723"] }, "apple_private_key": { "title": "Apple Private Key", @@ -581,12 +509,7 @@ } }, "additionalProperties": false, - "required": [ - "id", - "provider", - "client_id", - "mapper_url" - ], + "required": ["id", "provider", "client_id", "mapper_url"], "allOf": [ { "if": { @@ -595,23 +518,17 @@ "const": "microsoft" } }, - "required": [ - "provider" - ] + "required": ["provider"] }, "then": { - "required": [ - "microsoft_tenant" - ] + "required": ["microsoft_tenant"] }, "else": { "not": { "properties": { "microsoft_tenant": {} }, - "required": [ - "microsoft_tenant" - ] + "required": ["microsoft_tenant"] } } }, @@ -622,9 +539,7 @@ "const": "apple" } }, - "required": [ - "provider" - ] + "required": ["provider"] }, "then": { "not": { @@ -634,9 +549,7 @@ "minLength": 1 } }, - "required": [ - "client_secret" - ] + "required": ["client_secret"] }, "required": [ "apple_private_key_id", @@ -645,9 +558,7 @@ ] }, "else": { - "required": [ - "client_secret" - ], + "required": ["client_secret"], "allOf": [ { "not": { @@ -657,9 +568,7 @@ "minLength": 1 } }, - "required": [ - "apple_team_id" - ] + "required": ["apple_team_id"] } }, { @@ -670,9 +579,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key_id" - ] + "required": ["apple_private_key_id"] } }, { @@ -683,9 +590,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key" - ] + "required": ["apple_private_key"] } } ] @@ -826,10 +731,7 @@ "title": "Required Authenticator Assurance Level", "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.", "type": "string", - "enum": [ - "aal1", - "highest_available" - ], + "enum": ["aal1", "highest_available"], "default": "highest_available" }, "selfServiceAfterSettings": { @@ -875,6 +777,9 @@ "oidc": { "$ref": "#/definitions/selfServiceAfterOIDCLoginMethod" }, + "code": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, "hooks": { "type": "array", "items": { @@ -947,6 +852,9 @@ "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "code": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "hooks": { "$ref": "#/definitions/selfServiceHooks" } @@ -983,9 +891,7 @@ "path": { "title": "Path to PEM-encoded Fle", "type": "string", - "examples": [ - "path/to/file.pem" - ] + "examples": ["path/to/file.pem"] }, "base64": { "title": "Base64 Encoded Inline", @@ -1033,9 +939,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] }, "valid": { "additionalProperties": false, @@ -1045,9 +949,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } }, @@ -1097,9 +999,7 @@ "selfservice": { "type": "object", "additionalProperties": false, - "required": [ - "default_browser_return_url" - ], + "required": ["default_browser_return_url"], "properties": { "default_browser_return_url": { "$ref": "#/definitions/defaultReturnTo" @@ -1133,30 +1033,20 @@ "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/user/settings" - ], + "examples": ["https://my-app.com/user/settings"], "default": "https://www.ory.sh/kratos/docs/fallback/settings" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "privileged_session_max_age": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "required_aal": { "$ref": "#/definitions/featureRequiredAal" @@ -1205,20 +1095,14 @@ "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/signup" - ], + "examples": ["https://my-app.com/signup"], "default": "https://www.ory.sh/kratos/docs/fallback/registration" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeRegistration" @@ -1237,20 +1121,14 @@ "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/login" - ], + "examples": ["https://my-app.com/login"], "default": "https://www.ory.sh/kratos/docs/fallback/login" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" @@ -1276,9 +1154,7 @@ "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/verify" - ], + "examples": ["https://my-app.com/verify"], "default": "https://www.ory.sh/kratos/docs/fallback/verification" }, "after": { @@ -1290,11 +1166,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeVerification" @@ -1303,10 +1175,7 @@ "title": "Verification Strategy", "description": "The strategy to use for verification requests", "type": "string", - "enum": [ - "link", - "code" - ], + "enum": ["link", "code"], "default": "code" }, "notify_unknown_recipients": { @@ -1333,9 +1202,7 @@ "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/verify" - ], + "examples": ["https://my-app.com/verify"], "default": "https://www.ory.sh/kratos/docs/fallback/recovery" }, "after": { @@ -1347,11 +1214,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeRecovery" @@ -1360,10 +1223,7 @@ "title": "Recovery Strategy", "description": "The strategy to use for recovery requests", "type": "string", - "enum": [ - "link", - "code" - ], + "enum": ["link", "code"], "default": "code" }, "notify_unknown_recipients": { @@ -1383,9 +1243,7 @@ "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/kratos-error" - ], + "examples": ["https://my-app.com/kratos-error"], "default": "https://www.ory.sh/kratos/docs/fallback/error" } } @@ -1424,20 +1282,14 @@ "base_url": { "title": "Override the base URL which should be used as the base for recovery and verification links.", "type": "string", - "examples": [ - "https://my-app.com" - ] + "examples": ["https://my-app.com"] }, "lifespan": { "title": "How long a link is valid for", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } } @@ -1447,6 +1299,16 @@ "type": "object", "additionalProperties": false, "properties": { + "login_enabled": { + "type": "boolean", + "title": "Enables Login with Code Method", + "default": false + }, + "registration_enabled": { + "type": "boolean", + "title": "Enables Registration with Code Method", + "default": false + }, "enabled": { "type": "boolean", "title": "Enables Code Method", @@ -1462,11 +1324,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } } @@ -1589,17 +1447,13 @@ "type": "string", "title": "Relying Party Display Name", "description": "An name to help the user identify this RP.", - "examples": [ - "Ory Foundation" - ] + "examples": ["Ory Foundation"] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": [ - "ory.sh" - ] + "examples": ["ory.sh"] }, "origin": { "type": "string", @@ -1607,9 +1461,7 @@ "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).", "format": "uri", "deprecationMessage": "This field is deprecated. Use `origins` instead.", - "examples": [ - "https://www.ory.sh" - ] + "examples": ["https://www.ory.sh"] }, "origins": { "type": "array", @@ -1630,43 +1482,33 @@ "description": "An icon to help the user identify this RP.", "format": "uri", "deprecationMessage": "This field is deprecated and ignored due to security considerations.", - "examples": [ - "https://www.ory.sh/an-icon.png" - ] + "examples": ["https://www.ory.sh/an-icon.png"] } }, "type": "object", "oneOf": [ { - "required": [ - "id", - "display_name" - ], + "required": ["id", "display_name"], "properties": { - "origin": {"not": {}}, - "origins": {"not": {}} + "origin": { "not": {} }, + "origins": { "not": {} } } }, { - "required": [ - "id", - "display_name", - "origin" - ], + "required": ["id", "display_name", "origin"], "properties": { - "origin": {"type": "string"}, - "origins": {"not": {}} + "origin": { "type": "string" }, + "origins": { "not": {} } } }, { - "required": [ - "id", - "display_name", - "origins" - ], + "required": ["id", "display_name", "origins"], "properties": { - "origin": {"not": {}}, - "origins": {"type": "array", "items": {"type": "string"}} + "origin": { "not": {} }, + "origins": { + "type": "array", + "items": { "type": "string" } + } } } ] @@ -1681,14 +1523,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] }, "then": { - "required": [ - "config" - ] + "required": ["config"] } }, "oidc": { @@ -1711,9 +1549,7 @@ "title": "Base URL for OAuth2 Redirect URIs", "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.", "format": "uri", - "examples": [ - "https://auth.myexample.org/" - ] + "examples": ["https://auth.myexample.org/"] }, "providers": { "title": "OpenID Connect and OAuth2 Providers", @@ -1812,18 +1648,13 @@ "type": "string", "title": "Override message templates", "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", - "examples": [ - "/conf/courier-templates" - ] + "examples": ["/conf/courier-templates"] }, "message_retries": { "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned", "type": "integer", "default": 5, - "examples": [ - 10, - 60 - ] + "examples": [10, 60] }, "delivery_strategy": { "title": "Delivery Strategy", @@ -1886,9 +1717,7 @@ "title": "SMTP Sender Name", "description": "The recipient of an email will see this as the sender name.", "type": "string", - "examples": [ - "Bob" - ] + "examples": ["Bob"] }, "headers": { "title": "SMTP Headers", @@ -1912,9 +1741,7 @@ "default": "localhost" } }, - "required": [ - "connection_uri" - ], + "required": ["connection_uri"], "additionalProperties": false }, "sms": { @@ -1939,9 +1766,7 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to connect to the SMS provider.", - "examples": [ - "https://api.twillio.com/sms/send" - ], + "examples": ["https://api.twillio.com/sms/send"], "type": "string", "pattern": "^https?:\\/\\/.*" }, @@ -1983,19 +1808,14 @@ }, "additionalProperties": false }, - "required": [ - "url", - "method" - ], + "required": ["url", "method"], "additionalProperties": false } }, "additionalProperties": false } }, - "required": [ - "smtp" - ], + "required": ["smtp"], "additionalProperties": false }, "oauth2_provider": { @@ -2026,10 +1846,10 @@ ] }, "override_return_to": { - "title":"Persist OAuth2 request between flows", - "type":"boolean", - "default":false, - "description":"Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow." + "title": "Persist OAuth2 request between flows", + "type": "boolean", + "default": false, + "description": "Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow." } }, "additionalProperties": false @@ -2057,9 +1877,7 @@ "description": "The URL where the admin endpoint is exposed at.", "type": "string", "format": "uri", - "examples": [ - "https://kratos.private-network:4434/" - ] + "examples": ["https://kratos.private-network:4434/"] }, "host": { "title": "Admin Host", @@ -2073,9 +1891,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 4434 }, "socket": { @@ -2134,9 +1950,7 @@ ] }, "uniqueItems": true, - "default": [ - "*" - ], + "default": ["*"], "examples": [ [ "https://example.com", @@ -2148,13 +1962,7 @@ "allowed_methods": { "type": "array", "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", - "default": [ - "POST", - "GET", - "PUT", - "PATCH", - "DELETE" - ], + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], "items": { "type": "string", "enum": [ @@ -2185,9 +1993,7 @@ "exposed_headers": { "type": "array", "description": "Sets which headers are safe to expose to the API of a CORS API specification.", - "default": [ - "Content-Type" - ], + "default": ["Content-Type"], "items": { "type": "string" } @@ -2230,9 +2036,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4433 - ], + "examples": [4433], "default": 4433 }, "socket": { @@ -2282,10 +2086,7 @@ "format": { "description": "The log format can either be text or JSON.", "type": "string", - "enum": [ - "json", - "text" - ] + "enum": ["json", "text"] } }, "additionalProperties": false @@ -2326,9 +2127,7 @@ "id": { "title": "The schema's ID.", "type": "string", - "examples": [ - "employee" - ] + "examples": ["employee"] }, "url": { "type": "string", @@ -2342,16 +2141,11 @@ ] } }, - "required": [ - "id", - "url" - ] + "required": ["id", "url"] } } }, - "required": [ - "schemas" - ], + "required": ["schemas"], "additionalProperties": false }, "secrets": { @@ -2400,10 +2194,7 @@ "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.", "type": "string", "default": "bcrypt", - "enum": [ - "argon2", - "bcrypt" - ] + "enum": ["argon2", "bcrypt"] }, "argon2": { "title": "Configuration for the Argon2id hasher.", @@ -2459,9 +2250,7 @@ "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.", "type": "object", "additionalProperties": false, - "required": [ - "cost" - ], + "required": ["cost"], "properties": { "cost": { "type": "integer", @@ -2483,11 +2272,7 @@ "description": "One of the values: noop, aes, xchacha20-poly1305", "type": "string", "default": "noop", - "enum": [ - "noop", - "aes", - "xchacha20-poly1305" - ] + "enum": ["noop", "aes", "xchacha20-poly1305"] } } }, @@ -2511,11 +2296,7 @@ "title": "HTTP Cookie Same Site Configuration", "description": "Sets the session and CSRF cookie SameSite.", "type": "string", - "enum": [ - "Strict", - "Lax", - "None" - ], + "enum": ["Strict", "Lax", "None"], "default": "Lax" } }, @@ -2542,11 +2323,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "cookie": { "type": "object", @@ -2577,11 +2354,7 @@ "title": "Session Cookie SameSite Configuration", "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.", "type": "string", - "enum": [ - "Strict", - "Lax", - "None" - ] + "enum": ["Strict", "Lax", "None"] } }, "additionalProperties": false @@ -2592,11 +2365,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } }, @@ -2605,9 +2374,7 @@ "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", "type": "string", "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$", - "examples": [ - "v0.5.0-alpha.1" - ] + "examples": ["v0.5.0-alpha.1"] }, "dev": { "type": "boolean" @@ -2631,9 +2398,7 @@ "type": "integer", "minimum": 0, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 0 }, "config": { @@ -2702,14 +2467,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "verification" - ] + "required": ["verification"] }, { "properties": { @@ -2719,31 +2480,21 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "recovery" - ] + "required": ["recovery"] } ] } }, - "required": [ - "flows" - ] + "required": ["flows"] } }, - "required": [ - "selfservice" - ] + "required": ["selfservice"] }, "then": { - "required": [ - "courier" - ] + "required": ["courier"] } }, { @@ -2762,33 +2513,21 @@ ] } }, - "required": [ - "algorithm" - ] + "required": ["algorithm"] } }, - "required": [ - "ciphers" - ] + "required": ["ciphers"] }, "then": { - "required": [ - "secrets" - ], + "required": ["secrets"], "properties": { "secrets": { - "required": [ - "cipher" - ] + "required": ["cipher"] } } } } ], - "required": [ - "identity", - "dsn", - "selfservice" - ], + "required": ["identity", "dsn", "selfservice"], "additionalProperties": false } diff --git a/embedx/identity_extension.schema.json b/embedx/identity_extension.schema.json index ef402cdadcfb..9af2e97f07ce 100644 --- a/embedx/identity_extension.schema.json +++ b/embedx/identity_extension.schema.json @@ -38,6 +38,19 @@ "type": "boolean" } } + }, + "code": { + "type": "object", + "additionalProperties": false, + "properties": { + "identifier": { + "type": "boolean" + }, + "via": { + "type": "string", + "enum": ["email"] + } + } } } }, @@ -47,10 +60,7 @@ "properties": { "via": { "type": "string", - "enum": [ - "email", - "phone" - ] + "enum": ["email", "phone"] } } }, @@ -60,9 +70,7 @@ "properties": { "via": { "type": "string", - "enum": [ - "email" - ] + "enum": ["email"] } } } diff --git a/identity/credentials.go b/identity/credentials.go index 4efea49611f7..29283b29aa66 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -117,6 +117,7 @@ var AllCredentialTypes = []CredentialsType{ CredentialsTypeTOTP, CredentialsTypeLookup, CredentialsTypeWebAuthn, + CredentialsTypeCodeAuth, } const ( @@ -145,6 +146,15 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { return "", false } +// swagger:ignore +type CredentialsIdentifierAddressType string + +const ( + CredentialsIdentifierAddressTypeEmail CredentialsIdentifierAddressType = AddressTypeEmail + CredentialsIdentifierAddressTypePhone CredentialsIdentifierAddressType = AddressTypePhone + CredentialsIdentifierAddressTypeNone CredentialsIdentifierAddressType = "none" +) + // Credentials represents a specific credential type // // swagger:model identityCredentials @@ -158,6 +168,12 @@ type Credentials struct { // Identifiers represents a list of unique identifiers this credential type matches. Identifiers []string `json:"identifiers" db:"-"` + // IdentifierAddressType represents the type of the identifiers (e.g. email, phone). + // This is used to determine the correct courier to send messages to. + // The value is set by the code extension schema and is not persisted. + // only applicable on the login, registration with `code` method. + IdentifierAddressType CredentialsIdentifierAddressType `json:"-" db:"-"` + // Config contains the concrete credential payload. This might contain the bcrypt-hashed password, the email // for passwordless authentication or access_token and refresh tokens from OpenID Connect flows. Config sqlxx.JSONRawMessage `json:"config,omitempty" db:"config"` diff --git a/identity/credentials_code.go b/identity/credentials_code.go index b66d0964bbd9..b6fc4a14b4fc 100644 --- a/identity/credentials_code.go +++ b/identity/credentials_code.go @@ -1,9 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package identity -// CredentialsOTP represents an OTP code +import ( + "database/sql" +) + +type CodeAddressType string + +const ( + CodeAddressTypeEmail CodeAddressType = AddressTypeEmail + CodeAddressTypePhone CodeAddressType = AddressTypePhone +) + +// CredentialsCode represents a one time login/registraiton code // -// swagger:model identityCredentialsOTP -type CredentialsOTP struct { - // CodeHMAC represents the HMACed value of the login/registration code - CodeHMAC string `json:"code_hmac"` +// swagger:model identityCredentialsCode +type CredentialsCode struct { + // The type of the address for this code + AddressType CodeAddressType `json:"address_type"` + + // UsedAt indicates whether and when a recovery code was used. + UsedAt sql.NullTime `json:"used_at,omitempty"` } diff --git a/identity/extension_credentials.go b/identity/extension_credentials.go index 95a1ee8d4c93..7885abf10bce 100644 --- a/identity/extension_credentials.go +++ b/identity/extension_credentials.go @@ -11,6 +11,7 @@ import ( "github.com/ory/jsonschema/v3" "github.com/ory/x/sqlxx" "github.com/ory/x/stringslice" + "github.com/ory/x/stringsx" "github.com/ory/kratos/schema" ) @@ -25,7 +26,7 @@ func NewSchemaExtensionCredentials(i *Identity) *SchemaExtensionCredentials { return &SchemaExtensionCredentials{i: i} } -func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}) { +func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}, addressType CredentialsIdentifierAddressType) { cred, ok := r.i.GetCredentials(ct) if !ok { cred = &Credentials{ @@ -40,19 +41,39 @@ func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value int r.v[ct] = stringslice.Unique(append(r.v[ct], strings.ToLower(fmt.Sprintf("%s", value)))) cred.Identifiers = r.v[ct] + cred.IdentifierAddressType = addressType r.i.SetCredentials(ct, *cred) } -func (r *SchemaExtensionCredentials) Run(_ jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { +func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { r.l.Lock() defer r.l.Unlock() if s.Credentials.Password.Identifier { - r.setIdentifier(CredentialsTypePassword, value) + r.setIdentifier(CredentialsTypePassword, value, CredentialsIdentifierAddressTypeNone) } if s.Credentials.WebAuthn.Identifier { - r.setIdentifier(CredentialsTypeWebAuthn, value) + r.setIdentifier(CredentialsTypeWebAuthn, value, CredentialsIdentifierAddressTypeNone) + } + + if s.Credentials.Code.Identifier { + switch f := stringsx.SwitchExact(s.Credentials.Code.Via); { + case f.AddCase(AddressTypeEmail): + if !jsonschema.Formats["email"](value) { + return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) + } + + r.setIdentifier(CredentialsTypeCodeAuth, value, CredentialsIdentifierAddressType(AddressTypeEmail)) + // case f.AddCase(AddressTypePhone): + // if !jsonschema.Formats["tel"](value) { + // return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) + // } + // + // r.setIdentifier(CredentialsTypeCodeAuth, value, CredentialsIdentifierAddressType(AddressTypePhone)) + default: + return ctx.Error("", "credentials.code.via has unknown value %q", s.Credentials.Code.Via) + } } return nil diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index f7968b85c90e..ce02e19c5665 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -34,6 +34,7 @@ docs/HealthStatus.md docs/Identity.md docs/IdentityApi.md docs/IdentityCredentials.md +docs/IdentityCredentialsOTP.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -52,6 +53,7 @@ docs/IsAlive200Response.md docs/IsReady503Response.md docs/JsonPatch.md docs/LoginFlow.md +docs/LoginFlowState.md docs/LogoutFlow.md docs/Message.md docs/MessageDispatch.md @@ -69,6 +71,7 @@ docs/RecoveryFlowState.md docs/RecoveryIdentityAddress.md docs/RecoveryLinkForIdentity.md docs/RegistrationFlow.md +docs/RegistrationFlowState.md docs/SelfServiceFlowExpiredError.md docs/Session.md docs/SessionAuthenticationMethod.md @@ -92,6 +95,7 @@ docs/UiNodeTextAttributes.md docs/UiText.md docs/UpdateIdentityBody.md docs/UpdateLoginFlowBody.md +docs/UpdateLoginFlowWithCodeMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md docs/UpdateLoginFlowWithPasswordMethod.md @@ -101,6 +105,7 @@ docs/UpdateRecoveryFlowBody.md docs/UpdateRecoveryFlowWithCodeMethod.md docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md +docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md @@ -146,6 +151,7 @@ model_identity.go model_identity_credentials.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go +model_identity_credentials_otp.go model_identity_credentials_password.go model_identity_credentials_type.go model_identity_patch.go @@ -162,6 +168,7 @@ model_is_alive_200_response.go model_is_ready_503_response.go model_json_patch.go model_login_flow.go +model_login_flow_state.go model_logout_flow.go model_message.go model_message_dispatch.go @@ -178,6 +185,7 @@ model_recovery_flow_state.go model_recovery_identity_address.go model_recovery_link_for_identity.go model_registration_flow.go +model_registration_flow_state.go model_self_service_flow_expired_error.go model_session.go model_session_authentication_method.go @@ -201,6 +209,7 @@ model_ui_node_text_attributes.go model_ui_text.go model_update_identity_body.go model_update_login_flow_body.go +model_update_login_flow_with_code_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go model_update_login_flow_with_password_method.go @@ -210,6 +219,7 @@ model_update_recovery_flow_body.go model_update_recovery_flow_with_code_method.go model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go +model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go model_update_registration_flow_with_password_method.go model_update_registration_flow_with_web_authn_method.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index cb48b260e91f..76c437bd3751 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -159,6 +159,7 @@ Class | Method | HTTP request | Description - [HealthStatus](docs/HealthStatus.md) - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) + - [IdentityCredentialsOTP](docs/IdentityCredentialsOTP.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) @@ -177,6 +178,7 @@ Class | Method | HTTP request | Description - [IsReady503Response](docs/IsReady503Response.md) - [JsonPatch](docs/JsonPatch.md) - [LoginFlow](docs/LoginFlow.md) + - [LoginFlowState](docs/LoginFlowState.md) - [LogoutFlow](docs/LogoutFlow.md) - [Message](docs/Message.md) - [MessageDispatch](docs/MessageDispatch.md) @@ -193,6 +195,7 @@ Class | Method | HTTP request | Description - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) - [RegistrationFlow](docs/RegistrationFlow.md) + - [RegistrationFlowState](docs/RegistrationFlowState.md) - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) - [Session](docs/Session.md) - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) @@ -216,6 +219,7 @@ Class | Method | HTTP request | Description - [UiText](docs/UiText.md) - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) + - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) @@ -225,6 +229,7 @@ Class | Method | HTTP request | Description - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) + - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) diff --git a/internal/client-go/model_identity_credentials_otp.go b/internal/client-go/model_identity_credentials_otp.go new file mode 100644 index 000000000000..b60601987e67 --- /dev/null +++ b/internal/client-go/model_identity_credentials_otp.go @@ -0,0 +1,162 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "time" +) + +// IdentityCredentialsOTP CredentialsOTP represents an OTP code +type IdentityCredentialsOTP struct { + AddressType *string `json:"address_type,omitempty"` + UsedAt NullableTime `json:"used_at,omitempty"` +} + +// NewIdentityCredentialsOTP instantiates a new IdentityCredentialsOTP object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsOTP() *IdentityCredentialsOTP { + this := IdentityCredentialsOTP{} + return &this +} + +// NewIdentityCredentialsOTPWithDefaults instantiates a new IdentityCredentialsOTP object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsOTPWithDefaults() *IdentityCredentialsOTP { + this := IdentityCredentialsOTP{} + return &this +} + +// GetAddressType returns the AddressType field value if set, zero value otherwise. +func (o *IdentityCredentialsOTP) GetAddressType() string { + if o == nil || o.AddressType == nil { + var ret string + return ret + } + return *o.AddressType +} + +// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOTP) GetAddressTypeOk() (*string, bool) { + if o == nil || o.AddressType == nil { + return nil, false + } + return o.AddressType, true +} + +// HasAddressType returns a boolean if a field has been set. +func (o *IdentityCredentialsOTP) HasAddressType() bool { + if o != nil && o.AddressType != nil { + return true + } + + return false +} + +// SetAddressType gets a reference to the given string and assigns it to the AddressType field. +func (o *IdentityCredentialsOTP) SetAddressType(v string) { + o.AddressType = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityCredentialsOTP) GetUsedAt() time.Time { + if o == nil || o.UsedAt.Get() == nil { + var ret time.Time + return ret + } + return *o.UsedAt.Get() +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityCredentialsOTP) GetUsedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return o.UsedAt.Get(), o.UsedAt.IsSet() +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsOTP) HasUsedAt() bool { + if o != nil && o.UsedAt.IsSet() { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. +func (o *IdentityCredentialsOTP) SetUsedAt(v time.Time) { + o.UsedAt.Set(&v) +} + +// SetUsedAtNil sets the value for UsedAt to be an explicit nil +func (o *IdentityCredentialsOTP) SetUsedAtNil() { + o.UsedAt.Set(nil) +} + +// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil +func (o *IdentityCredentialsOTP) UnsetUsedAt() { + o.UsedAt.Unset() +} + +func (o IdentityCredentialsOTP) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.AddressType != nil { + toSerialize["address_type"] = o.AddressType + } + if o.UsedAt.IsSet() { + toSerialize["used_at"] = o.UsedAt.Get() + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsOTP struct { + value *IdentityCredentialsOTP + isSet bool +} + +func (v NullableIdentityCredentialsOTP) Get() *IdentityCredentialsOTP { + return v.value +} + +func (v *NullableIdentityCredentialsOTP) Set(val *IdentityCredentialsOTP) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsOTP) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsOTP) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsOTP(val *IdentityCredentialsOTP) *NullableIdentityCredentialsOTP { + return &NullableIdentityCredentialsOTP{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsOTP) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsOTP) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_login_flow.go b/internal/client-go/model_login_flow.go index 1b3f4b6c7dde..fb27ee68236d 100644 --- a/internal/client-go/model_login_flow.go +++ b/internal/client-go/model_login_flow.go @@ -39,6 +39,8 @@ type LoginFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the login flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -50,12 +52,13 @@ type LoginFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *LoginFlow { +func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *LoginFlow { this := LoginFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -421,6 +424,32 @@ func (o *LoginFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *LoginFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *LoginFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *LoginFlow) SetState(v interface{}) { + o.State = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -539,6 +568,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_login_flow_state.go b/internal/client-go/model_login_flow_state.go new file mode 100644 index 000000000000..0dd0545f031c --- /dev/null +++ b/internal/client-go/model_login_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// LoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +type LoginFlowState string + +// List of LoginFlowState +const ( + LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" + LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" + LOGINFLOWSTATE_PASSED_CHALLENGE LoginFlowState = "passed_challenge" +) + +func (v *LoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := LoginFlowState(value) + for _, existing := range []LoginFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid LoginFlowState", value) +} + +// Ptr returns reference to LoginFlowState value +func (v LoginFlowState) Ptr() *LoginFlowState { + return &v +} + +type NullableLoginFlowState struct { + value *LoginFlowState + isSet bool +} + +func (v NullableLoginFlowState) Get() *LoginFlowState { + return v.value +} + +func (v *NullableLoginFlowState) Set(val *LoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowState(val *LoginFlowState) *NullableLoginFlowState { + return &NullableLoginFlowState{value: val, isSet: true} +} + +func (v NullableLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_message.go b/internal/client-go/model_message.go index 3a6f3f92362b..f0452185f169 100644 --- a/internal/client-go/model_message.go +++ b/internal/client-go/model_message.go @@ -28,7 +28,7 @@ type Message struct { SendCount int64 `json:"send_count"` Status CourierMessageStatus `json:"status"` Subject string `json:"subject"` - // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid TemplateType string `json:"template_type"` Type CourierMessageType `json:"type"` // UpdatedAt is a helper struct field for gobuffalo.pop. diff --git a/internal/client-go/model_recovery_flow.go b/internal/client-go/model_recovery_flow.go index 6ae19ebd60e6..acf4ff667df3 100644 --- a/internal/client-go/model_recovery_flow.go +++ b/internal/client-go/model_recovery_flow.go @@ -29,8 +29,9 @@ type RecoveryFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State RecoveryFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type RecoveryFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state RecoveryFlowState, type_ string, ui UiContainer) *RecoveryFlow { +func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RecoveryFlow { this := RecoveryFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -221,9 +222,10 @@ func (o *RecoveryFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *RecoveryFlow) GetState() RecoveryFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RecoveryFlow) GetState() interface{} { if o == nil { - var ret RecoveryFlowState + var ret interface{} return ret } @@ -232,15 +234,16 @@ func (o *RecoveryFlow) GetState() RecoveryFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *RecoveryFlow) GetStateOk() (*RecoveryFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RecoveryFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *RecoveryFlow) SetState(v RecoveryFlowState) { +func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } @@ -312,7 +315,7 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/client-go/model_recovery_flow_state.go b/internal/client-go/model_recovery_flow_state.go index 1c660ba043b9..53f95534661d 100644 --- a/internal/client-go/model_recovery_flow_state.go +++ b/internal/client-go/model_recovery_flow_state.go @@ -19,7 +19,7 @@ import ( // RecoveryFlowState The state represents the state of the recovery flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type RecoveryFlowState string -// List of recoveryFlowState +// List of RecoveryFlowState const ( RECOVERYFLOWSTATE_CHOOSE_METHOD RecoveryFlowState = "choose_method" RECOVERYFLOWSTATE_SENT_EMAIL RecoveryFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *RecoveryFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid RecoveryFlowState", value) } -// Ptr returns reference to recoveryFlowState value +// Ptr returns reference to RecoveryFlowState value func (v RecoveryFlowState) Ptr() *RecoveryFlowState { return &v } diff --git a/internal/client-go/model_registration_flow.go b/internal/client-go/model_registration_flow.go index fe9f697b5551..9b08288d6a16 100644 --- a/internal/client-go/model_registration_flow.go +++ b/internal/client-go/model_registration_flow.go @@ -34,6 +34,8 @@ type RegistrationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. + State interface{} `json:"state"` // TransientPayload is used to pass data from the registration to a webhook TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. @@ -45,12 +47,13 @@ type RegistrationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *RegistrationFlow { +func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RegistrationFlow { this := RegistrationFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -320,6 +323,32 @@ func (o *RegistrationFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RegistrationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RegistrationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *RegistrationFlow) SetState(v interface{}) { + o.State = v +} + // GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. func (o *RegistrationFlow) GetTransientPayload() map[string]interface{} { if o == nil || o.TransientPayload == nil { @@ -429,6 +458,9 @@ func (o RegistrationFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if o.TransientPayload != nil { toSerialize["transient_payload"] = o.TransientPayload } diff --git a/internal/client-go/model_registration_flow_state.go b/internal/client-go/model_registration_flow_state.go new file mode 100644 index 000000000000..c3be9f33cd79 --- /dev/null +++ b/internal/client-go/model_registration_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// RegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +type RegistrationFlowState string + +// List of RegistrationFlowState +const ( + REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" + REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" + REGISTRATIONFLOWSTATE_PASSED_CHALLENGE RegistrationFlowState = "passed_challenge" +) + +func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RegistrationFlowState(value) + for _, existing := range []RegistrationFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) +} + +// Ptr returns reference to RegistrationFlowState value +func (v RegistrationFlowState) Ptr() *RegistrationFlowState { + return &v +} + +type NullableRegistrationFlowState struct { + value *RegistrationFlowState + isSet bool +} + +func (v NullableRegistrationFlowState) Get() *RegistrationFlowState { + return v.value +} + +func (v *NullableRegistrationFlowState) Set(val *RegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlowState(val *RegistrationFlowState) *NullableRegistrationFlowState { + return &NullableRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_self_service_login_flow_state.go b/internal/client-go/model_self_service_login_flow_state.go new file mode 100644 index 000000000000..093d300fe207 --- /dev/null +++ b/internal/client-go/model_self_service_login_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// SelfServiceLoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +type SelfServiceLoginFlowState string + +// List of SelfServiceLoginFlowState +const ( + SELFSERVICELOGINFLOWSTATE_CHOOSE_METHOD SelfServiceLoginFlowState = "choose_method" + SELFSERVICELOGINFLOWSTATE_SENT_EMAIL SelfServiceLoginFlowState = "sent_email" + SELFSERVICELOGINFLOWSTATE_PASSED_CHALLENGE SelfServiceLoginFlowState = "passed_challenge" +) + +func (v *SelfServiceLoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := SelfServiceLoginFlowState(value) + for _, existing := range []SelfServiceLoginFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid SelfServiceLoginFlowState", value) +} + +// Ptr returns reference to SelfServiceLoginFlowState value +func (v SelfServiceLoginFlowState) Ptr() *SelfServiceLoginFlowState { + return &v +} + +type NullableSelfServiceLoginFlowState struct { + value *SelfServiceLoginFlowState + isSet bool +} + +func (v NullableSelfServiceLoginFlowState) Get() *SelfServiceLoginFlowState { + return v.value +} + +func (v *NullableSelfServiceLoginFlowState) Set(val *SelfServiceLoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableSelfServiceLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableSelfServiceLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSelfServiceLoginFlowState(val *SelfServiceLoginFlowState) *NullableSelfServiceLoginFlowState { + return &NullableSelfServiceLoginFlowState{value: val, isSet: true} +} + +func (v NullableSelfServiceLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSelfServiceLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_self_service_recovery_flow_state.go b/internal/client-go/model_self_service_recovery_flow_state.go index efb98b8d127e..ae492a51d26b 100644 --- a/internal/client-go/model_self_service_recovery_flow_state.go +++ b/internal/client-go/model_self_service_recovery_flow_state.go @@ -1,13 +1,10 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - /* - * Ory Kratos API + * Ory Identities API * - * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. * * API version: - * Contact: hi@ory.sh + * Contact: office@ory.sh */ // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. @@ -22,7 +19,7 @@ import ( // SelfServiceRecoveryFlowState The state represents the state of the recovery flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type SelfServiceRecoveryFlowState string -// List of selfServiceRecoveryFlowState +// List of SelfServiceRecoveryFlowState const ( SELFSERVICERECOVERYFLOWSTATE_CHOOSE_METHOD SelfServiceRecoveryFlowState = "choose_method" SELFSERVICERECOVERYFLOWSTATE_SENT_EMAIL SelfServiceRecoveryFlowState = "sent_email" @@ -46,7 +43,7 @@ func (v *SelfServiceRecoveryFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SelfServiceRecoveryFlowState", value) } -// Ptr returns reference to selfServiceRecoveryFlowState value +// Ptr returns reference to SelfServiceRecoveryFlowState value func (v SelfServiceRecoveryFlowState) Ptr() *SelfServiceRecoveryFlowState { return &v } diff --git a/internal/client-go/model_self_service_registration_flow_state.go b/internal/client-go/model_self_service_registration_flow_state.go new file mode 100644 index 000000000000..a84387784ef1 --- /dev/null +++ b/internal/client-go/model_self_service_registration_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// SelfServiceRegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +type SelfServiceRegistrationFlowState string + +// List of SelfServiceRegistrationFlowState +const ( + SELFSERVICEREGISTRATIONFLOWSTATE_CHOOSE_METHOD SelfServiceRegistrationFlowState = "choose_method" + SELFSERVICEREGISTRATIONFLOWSTATE_SENT_EMAIL SelfServiceRegistrationFlowState = "sent_email" + SELFSERVICEREGISTRATIONFLOWSTATE_PASSED_CHALLENGE SelfServiceRegistrationFlowState = "passed_challenge" +) + +func (v *SelfServiceRegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := SelfServiceRegistrationFlowState(value) + for _, existing := range []SelfServiceRegistrationFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid SelfServiceRegistrationFlowState", value) +} + +// Ptr returns reference to SelfServiceRegistrationFlowState value +func (v SelfServiceRegistrationFlowState) Ptr() *SelfServiceRegistrationFlowState { + return &v +} + +type NullableSelfServiceRegistrationFlowState struct { + value *SelfServiceRegistrationFlowState + isSet bool +} + +func (v NullableSelfServiceRegistrationFlowState) Get() *SelfServiceRegistrationFlowState { + return v.value +} + +func (v *NullableSelfServiceRegistrationFlowState) Set(val *SelfServiceRegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableSelfServiceRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableSelfServiceRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSelfServiceRegistrationFlowState(val *SelfServiceRegistrationFlowState) *NullableSelfServiceRegistrationFlowState { + return &NullableSelfServiceRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableSelfServiceRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSelfServiceRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_self_service_settings_flow_state.go b/internal/client-go/model_self_service_settings_flow_state.go index 3a95e53bf169..9163efb643e5 100644 --- a/internal/client-go/model_self_service_settings_flow_state.go +++ b/internal/client-go/model_self_service_settings_flow_state.go @@ -1,13 +1,10 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - /* - * Ory Kratos API + * Ory Identities API * - * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. * * API version: - * Contact: hi@ory.sh + * Contact: office@ory.sh */ // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. @@ -22,7 +19,7 @@ import ( // SelfServiceSettingsFlowState show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. type SelfServiceSettingsFlowState string -// List of selfServiceSettingsFlowState +// List of SelfServiceSettingsFlowState const ( SELFSERVICESETTINGSFLOWSTATE_SHOW_FORM SelfServiceSettingsFlowState = "show_form" SELFSERVICESETTINGSFLOWSTATE_SUCCESS SelfServiceSettingsFlowState = "success" @@ -45,7 +42,7 @@ func (v *SelfServiceSettingsFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SelfServiceSettingsFlowState", value) } -// Ptr returns reference to selfServiceSettingsFlowState value +// Ptr returns reference to SelfServiceSettingsFlowState value func (v SelfServiceSettingsFlowState) Ptr() *SelfServiceSettingsFlowState { return &v } diff --git a/internal/client-go/model_self_service_verification_flow_state.go b/internal/client-go/model_self_service_verification_flow_state.go index 03937f84bd45..a3b9691ab038 100644 --- a/internal/client-go/model_self_service_verification_flow_state.go +++ b/internal/client-go/model_self_service_verification_flow_state.go @@ -1,13 +1,10 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - /* - * Ory Kratos API + * Ory Identities API * - * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. * * API version: - * Contact: hi@ory.sh + * Contact: office@ory.sh */ // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. @@ -22,7 +19,7 @@ import ( // SelfServiceVerificationFlowState The state represents the state of the verification flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type SelfServiceVerificationFlowState string -// List of selfServiceVerificationFlowState +// List of SelfServiceVerificationFlowState const ( SELFSERVICEVERIFICATIONFLOWSTATE_CHOOSE_METHOD SelfServiceVerificationFlowState = "choose_method" SELFSERVICEVERIFICATIONFLOWSTATE_SENT_EMAIL SelfServiceVerificationFlowState = "sent_email" @@ -46,7 +43,7 @@ func (v *SelfServiceVerificationFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SelfServiceVerificationFlowState", value) } -// Ptr returns reference to selfServiceVerificationFlowState value +// Ptr returns reference to SelfServiceVerificationFlowState value func (v SelfServiceVerificationFlowState) Ptr() *SelfServiceVerificationFlowState { return &v } diff --git a/internal/client-go/model_settings_flow.go b/internal/client-go/model_settings_flow.go index a1dc0aa98dc6..fa5cd9317c54 100644 --- a/internal/client-go/model_settings_flow.go +++ b/internal/client-go/model_settings_flow.go @@ -32,8 +32,9 @@ type SettingsFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State SettingsFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -43,7 +44,7 @@ type SettingsFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SettingsFlowState, type_ string, ui UiContainer) *SettingsFlow { +func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *SettingsFlow { this := SettingsFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -281,9 +282,10 @@ func (o *SettingsFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *SettingsFlow) GetState() SettingsFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *SettingsFlow) GetState() interface{} { if o == nil { - var ret SettingsFlowState + var ret interface{} return ret } @@ -292,15 +294,16 @@ func (o *SettingsFlow) GetState() SettingsFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *SettingsFlow) GetStateOk() (*SettingsFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *SettingsFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *SettingsFlow) SetState(v SettingsFlowState) { +func (o *SettingsFlow) SetState(v interface{}) { o.State = v } @@ -378,7 +381,7 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/client-go/model_settings_flow_state.go b/internal/client-go/model_settings_flow_state.go index f994c786a2d8..6d9e5b93f1fe 100644 --- a/internal/client-go/model_settings_flow_state.go +++ b/internal/client-go/model_settings_flow_state.go @@ -19,7 +19,7 @@ import ( // SettingsFlowState show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. type SettingsFlowState string -// List of settingsFlowState +// List of SettingsFlowState const ( SETTINGSFLOWSTATE_SHOW_FORM SettingsFlowState = "show_form" SETTINGSFLOWSTATE_SUCCESS SettingsFlowState = "success" @@ -42,7 +42,7 @@ func (v *SettingsFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SettingsFlowState", value) } -// Ptr returns reference to settingsFlowState value +// Ptr returns reference to SettingsFlowState value func (v SettingsFlowState) Ptr() *SettingsFlowState { return &v } diff --git a/internal/client-go/model_update_login_flow_with_code_method.go b/internal/client-go/model_update_login_flow_with_code_method.go new file mode 100644 index 000000000000..bd97ab583ebc --- /dev/null +++ b/internal/client-go/model_update_login_flow_with_code_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithCodeMethod Update Login flow using the code method +type UpdateLoginFlowWithCodeMethod struct { + // Code is the 6 digits code sent to the user + Code *string `json:"code,omitempty"` + // CSRFToken is the anti-CSRF token + CsrfToken string `json:"csrf_token"` + // Identifier is the code identifier The identifier requires that the user has already completed the registration or settings with code flow. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + Method string `json:"method"` + // Resend is set when the user wants to resend the code + Resend *string `json:"resend,omitempty"` +} + +// NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithCodeMethod(csrfToken string, method string) *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + this.CsrfToken = csrfToken + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithCodeMethodWithDefaults instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifier() string { + if o == nil || o.Identifier == nil { + var ret string + return ret + } + return *o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifierOk() (*string, bool) { + if o == nil || o.Identifier == nil { + return nil, false + } + return o.Identifier, true +} + +// HasIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasIdentifier() bool { + if o != nil && o.Identifier != nil { + return true + } + + return false +} + +// SetIdentifier gets a reference to the given string and assigns it to the Identifier field. +func (o *UpdateLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if true { + toSerialize["csrf_token"] = o.CsrfToken + } + if o.Identifier != nil { + toSerialize["identifier"] = o.Identifier + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithCodeMethod struct { + value *UpdateLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithCodeMethod) Get() *UpdateLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Set(val *UpdateLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithCodeMethod(val *UpdateLoginFlowWithCodeMethod) *NullableUpdateLoginFlowWithCodeMethod { + return &NullableUpdateLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_registration_flow_with_code_method.go b/internal/client-go/model_update_registration_flow_with_code_method.go new file mode 100644 index 000000000000..46b9126d666f --- /dev/null +++ b/internal/client-go/model_update_registration_flow_with_code_method.go @@ -0,0 +1,286 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithCodeMethod Update Registration Flow with Code Method +type UpdateRegistrationFlowWithCodeMethod struct { + // The OTP Code sent to the user + Code *string `json:"code,omitempty"` + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `code` when using the code method. + Method string `json:"method"` + // Resend restarts the flow with a new code + Resend *string `json:"resend,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithCodeMethod instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithCodeMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithCodeMethodWithDefaults instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithCodeMethodWithDefaults() *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithCodeMethod struct { + value *UpdateRegistrationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) Get() *UpdateRegistrationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Set(val *UpdateRegistrationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithCodeMethod(val *UpdateRegistrationFlowWithCodeMethod) *NullableUpdateRegistrationFlowWithCodeMethod { + return &NullableUpdateRegistrationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_verification_flow.go b/internal/client-go/model_verification_flow.go index 5190da660254..c10870c9f841 100644 --- a/internal/client-go/model_verification_flow.go +++ b/internal/client-go/model_verification_flow.go @@ -29,8 +29,9 @@ type VerificationFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl *string `json:"request_url,omitempty"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State VerificationFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type VerificationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewVerificationFlow(id string, state VerificationFlowState, type_ string, ui UiContainer) *VerificationFlow { +func NewVerificationFlow(id string, state interface{}, type_ string, ui UiContainer) *VerificationFlow { this := VerificationFlow{} this.Id = id this.State = state @@ -242,9 +243,10 @@ func (o *VerificationFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *VerificationFlow) GetState() VerificationFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *VerificationFlow) GetState() interface{} { if o == nil { - var ret VerificationFlowState + var ret interface{} return ret } @@ -253,15 +255,16 @@ func (o *VerificationFlow) GetState() VerificationFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *VerificationFlow) GetStateOk() (*VerificationFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *VerificationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *VerificationFlow) SetState(v VerificationFlowState) { +func (o *VerificationFlow) SetState(v interface{}) { o.State = v } @@ -333,7 +336,7 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/client-go/model_verification_flow_state.go b/internal/client-go/model_verification_flow_state.go index bea74568c94d..b34326eec3fc 100644 --- a/internal/client-go/model_verification_flow_state.go +++ b/internal/client-go/model_verification_flow_state.go @@ -19,7 +19,7 @@ import ( // VerificationFlowState The state represents the state of the verification flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type VerificationFlowState string -// List of verificationFlowState +// List of VerificationFlowState const ( VERIFICATIONFLOWSTATE_CHOOSE_METHOD VerificationFlowState = "choose_method" VERIFICATIONFLOWSTATE_SENT_EMAIL VerificationFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *VerificationFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid VerificationFlowState", value) } -// Ptr returns reference to verificationFlowState value +// Ptr returns reference to VerificationFlowState value func (v VerificationFlowState) Ptr() *VerificationFlowState { return &v } diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index af0c731e5f92..59f80226cbdb 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -35,6 +35,7 @@ docs/HealthStatus.md docs/Identity.md docs/IdentityApi.md docs/IdentityCredentials.md +docs/IdentityCredentialsOTP.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -53,6 +54,7 @@ docs/IsAlive200Response.md docs/IsReady503Response.md docs/JsonPatch.md docs/LoginFlow.md +docs/LoginFlowState.md docs/LogoutFlow.md docs/Message.md docs/MessageDispatch.md @@ -70,6 +72,7 @@ docs/RecoveryFlowState.md docs/RecoveryIdentityAddress.md docs/RecoveryLinkForIdentity.md docs/RegistrationFlow.md +docs/RegistrationFlowState.md docs/SelfServiceFlowExpiredError.md docs/Session.md docs/SessionAuthenticationMethod.md @@ -93,6 +96,7 @@ docs/UiNodeTextAttributes.md docs/UiText.md docs/UpdateIdentityBody.md docs/UpdateLoginFlowBody.md +docs/UpdateLoginFlowWithCodeMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md docs/UpdateLoginFlowWithPasswordMethod.md @@ -102,6 +106,7 @@ docs/UpdateRecoveryFlowBody.md docs/UpdateRecoveryFlowWithCodeMethod.md docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md +docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md @@ -147,6 +152,7 @@ model_identity.go model_identity_credentials.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go +model_identity_credentials_otp.go model_identity_credentials_password.go model_identity_credentials_type.go model_identity_patch.go @@ -163,6 +169,7 @@ model_is_alive_200_response.go model_is_ready_503_response.go model_json_patch.go model_login_flow.go +model_login_flow_state.go model_logout_flow.go model_message.go model_message_dispatch.go @@ -179,6 +186,7 @@ model_recovery_flow_state.go model_recovery_identity_address.go model_recovery_link_for_identity.go model_registration_flow.go +model_registration_flow_state.go model_self_service_flow_expired_error.go model_session.go model_session_authentication_method.go @@ -202,6 +210,7 @@ model_ui_node_text_attributes.go model_ui_text.go model_update_identity_body.go model_update_login_flow_body.go +model_update_login_flow_with_code_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go model_update_login_flow_with_password_method.go @@ -211,6 +220,7 @@ model_update_recovery_flow_body.go model_update_recovery_flow_with_code_method.go model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go +model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go model_update_registration_flow_with_password_method.go model_update_registration_flow_with_web_authn_method.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index cb48b260e91f..76c437bd3751 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -159,6 +159,7 @@ Class | Method | HTTP request | Description - [HealthStatus](docs/HealthStatus.md) - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) + - [IdentityCredentialsOTP](docs/IdentityCredentialsOTP.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) @@ -177,6 +178,7 @@ Class | Method | HTTP request | Description - [IsReady503Response](docs/IsReady503Response.md) - [JsonPatch](docs/JsonPatch.md) - [LoginFlow](docs/LoginFlow.md) + - [LoginFlowState](docs/LoginFlowState.md) - [LogoutFlow](docs/LogoutFlow.md) - [Message](docs/Message.md) - [MessageDispatch](docs/MessageDispatch.md) @@ -193,6 +195,7 @@ Class | Method | HTTP request | Description - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) - [RegistrationFlow](docs/RegistrationFlow.md) + - [RegistrationFlowState](docs/RegistrationFlowState.md) - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) - [Session](docs/Session.md) - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) @@ -216,6 +219,7 @@ Class | Method | HTTP request | Description - [UiText](docs/UiText.md) - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) + - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) @@ -225,6 +229,7 @@ Class | Method | HTTP request | Description - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) + - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) diff --git a/internal/httpclient/model_identity_credentials_otp.go b/internal/httpclient/model_identity_credentials_otp.go new file mode 100644 index 000000000000..b60601987e67 --- /dev/null +++ b/internal/httpclient/model_identity_credentials_otp.go @@ -0,0 +1,162 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "time" +) + +// IdentityCredentialsOTP CredentialsOTP represents an OTP code +type IdentityCredentialsOTP struct { + AddressType *string `json:"address_type,omitempty"` + UsedAt NullableTime `json:"used_at,omitempty"` +} + +// NewIdentityCredentialsOTP instantiates a new IdentityCredentialsOTP object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsOTP() *IdentityCredentialsOTP { + this := IdentityCredentialsOTP{} + return &this +} + +// NewIdentityCredentialsOTPWithDefaults instantiates a new IdentityCredentialsOTP object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsOTPWithDefaults() *IdentityCredentialsOTP { + this := IdentityCredentialsOTP{} + return &this +} + +// GetAddressType returns the AddressType field value if set, zero value otherwise. +func (o *IdentityCredentialsOTP) GetAddressType() string { + if o == nil || o.AddressType == nil { + var ret string + return ret + } + return *o.AddressType +} + +// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsOTP) GetAddressTypeOk() (*string, bool) { + if o == nil || o.AddressType == nil { + return nil, false + } + return o.AddressType, true +} + +// HasAddressType returns a boolean if a field has been set. +func (o *IdentityCredentialsOTP) HasAddressType() bool { + if o != nil && o.AddressType != nil { + return true + } + + return false +} + +// SetAddressType gets a reference to the given string and assigns it to the AddressType field. +func (o *IdentityCredentialsOTP) SetAddressType(v string) { + o.AddressType = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityCredentialsOTP) GetUsedAt() time.Time { + if o == nil || o.UsedAt.Get() == nil { + var ret time.Time + return ret + } + return *o.UsedAt.Get() +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityCredentialsOTP) GetUsedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return o.UsedAt.Get(), o.UsedAt.IsSet() +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsOTP) HasUsedAt() bool { + if o != nil && o.UsedAt.IsSet() { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. +func (o *IdentityCredentialsOTP) SetUsedAt(v time.Time) { + o.UsedAt.Set(&v) +} + +// SetUsedAtNil sets the value for UsedAt to be an explicit nil +func (o *IdentityCredentialsOTP) SetUsedAtNil() { + o.UsedAt.Set(nil) +} + +// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil +func (o *IdentityCredentialsOTP) UnsetUsedAt() { + o.UsedAt.Unset() +} + +func (o IdentityCredentialsOTP) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.AddressType != nil { + toSerialize["address_type"] = o.AddressType + } + if o.UsedAt.IsSet() { + toSerialize["used_at"] = o.UsedAt.Get() + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsOTP struct { + value *IdentityCredentialsOTP + isSet bool +} + +func (v NullableIdentityCredentialsOTP) Get() *IdentityCredentialsOTP { + return v.value +} + +func (v *NullableIdentityCredentialsOTP) Set(val *IdentityCredentialsOTP) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsOTP) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsOTP) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsOTP(val *IdentityCredentialsOTP) *NullableIdentityCredentialsOTP { + return &NullableIdentityCredentialsOTP{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsOTP) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsOTP) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_login_flow.go b/internal/httpclient/model_login_flow.go index 1b3f4b6c7dde..fb27ee68236d 100644 --- a/internal/httpclient/model_login_flow.go +++ b/internal/httpclient/model_login_flow.go @@ -39,6 +39,8 @@ type LoginFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the login flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -50,12 +52,13 @@ type LoginFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *LoginFlow { +func NewLoginFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *LoginFlow { this := LoginFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -421,6 +424,32 @@ func (o *LoginFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *LoginFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *LoginFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *LoginFlow) SetState(v interface{}) { + o.State = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -539,6 +568,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_login_flow_state.go b/internal/httpclient/model_login_flow_state.go new file mode 100644 index 000000000000..0dd0545f031c --- /dev/null +++ b/internal/httpclient/model_login_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// LoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +type LoginFlowState string + +// List of LoginFlowState +const ( + LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" + LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" + LOGINFLOWSTATE_PASSED_CHALLENGE LoginFlowState = "passed_challenge" +) + +func (v *LoginFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := LoginFlowState(value) + for _, existing := range []LoginFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid LoginFlowState", value) +} + +// Ptr returns reference to LoginFlowState value +func (v LoginFlowState) Ptr() *LoginFlowState { + return &v +} + +type NullableLoginFlowState struct { + value *LoginFlowState + isSet bool +} + +func (v NullableLoginFlowState) Get() *LoginFlowState { + return v.value +} + +func (v *NullableLoginFlowState) Set(val *LoginFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableLoginFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableLoginFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLoginFlowState(val *LoginFlowState) *NullableLoginFlowState { + return &NullableLoginFlowState{value: val, isSet: true} +} + +func (v NullableLoginFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLoginFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_message.go b/internal/httpclient/model_message.go index 3a6f3f92362b..f0452185f169 100644 --- a/internal/httpclient/model_message.go +++ b/internal/httpclient/model_message.go @@ -28,7 +28,7 @@ type Message struct { SendCount int64 `json:"send_count"` Status CourierMessageStatus `json:"status"` Subject string `json:"subject"` - // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid TemplateType string `json:"template_type"` Type CourierMessageType `json:"type"` // UpdatedAt is a helper struct field for gobuffalo.pop. diff --git a/internal/httpclient/model_recovery_flow.go b/internal/httpclient/model_recovery_flow.go index 6ae19ebd60e6..acf4ff667df3 100644 --- a/internal/httpclient/model_recovery_flow.go +++ b/internal/httpclient/model_recovery_flow.go @@ -29,8 +29,9 @@ type RecoveryFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State RecoveryFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type RecoveryFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state RecoveryFlowState, type_ string, ui UiContainer) *RecoveryFlow { +func NewRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RecoveryFlow { this := RecoveryFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -221,9 +222,10 @@ func (o *RecoveryFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *RecoveryFlow) GetState() RecoveryFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RecoveryFlow) GetState() interface{} { if o == nil { - var ret RecoveryFlowState + var ret interface{} return ret } @@ -232,15 +234,16 @@ func (o *RecoveryFlow) GetState() RecoveryFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *RecoveryFlow) GetStateOk() (*RecoveryFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RecoveryFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *RecoveryFlow) SetState(v RecoveryFlowState) { +func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } @@ -312,7 +315,7 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/httpclient/model_recovery_flow_state.go b/internal/httpclient/model_recovery_flow_state.go index 1c660ba043b9..53f95534661d 100644 --- a/internal/httpclient/model_recovery_flow_state.go +++ b/internal/httpclient/model_recovery_flow_state.go @@ -19,7 +19,7 @@ import ( // RecoveryFlowState The state represents the state of the recovery flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type RecoveryFlowState string -// List of recoveryFlowState +// List of RecoveryFlowState const ( RECOVERYFLOWSTATE_CHOOSE_METHOD RecoveryFlowState = "choose_method" RECOVERYFLOWSTATE_SENT_EMAIL RecoveryFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *RecoveryFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid RecoveryFlowState", value) } -// Ptr returns reference to recoveryFlowState value +// Ptr returns reference to RecoveryFlowState value func (v RecoveryFlowState) Ptr() *RecoveryFlowState { return &v } diff --git a/internal/httpclient/model_registration_flow.go b/internal/httpclient/model_registration_flow.go index fe9f697b5551..9b08288d6a16 100644 --- a/internal/httpclient/model_registration_flow.go +++ b/internal/httpclient/model_registration_flow.go @@ -34,6 +34,8 @@ type RegistrationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed. This is only set if the client has requested a session token exchange code, and if the flow is of type \"api\", and only on creating the flow. SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. + State interface{} `json:"state"` // TransientPayload is used to pass data from the registration to a webhook TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. @@ -45,12 +47,13 @@ type RegistrationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *RegistrationFlow { +func NewRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *RegistrationFlow { this := RegistrationFlow{} this.ExpiresAt = expiresAt this.Id = id this.IssuedAt = issuedAt this.RequestUrl = requestUrl + this.State = state this.Type = type_ this.Ui = ui return &this @@ -320,6 +323,32 @@ func (o *RegistrationFlow) SetSessionTokenExchangeCode(v string) { o.SessionTokenExchangeCode = &v } +// GetState returns the State field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *RegistrationFlow) GetState() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.State +} + +// GetStateOk returns a tuple with the State field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *RegistrationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { + return nil, false + } + return &o.State, true +} + +// SetState sets field value +func (o *RegistrationFlow) SetState(v interface{}) { + o.State = v +} + // GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. func (o *RegistrationFlow) GetTransientPayload() map[string]interface{} { if o == nil || o.TransientPayload == nil { @@ -429,6 +458,9 @@ func (o RegistrationFlow) MarshalJSON() ([]byte, error) { if o.SessionTokenExchangeCode != nil { toSerialize["session_token_exchange_code"] = o.SessionTokenExchangeCode } + if o.State != nil { + toSerialize["state"] = o.State + } if o.TransientPayload != nil { toSerialize["transient_payload"] = o.TransientPayload } diff --git a/internal/httpclient/model_registration_flow_state.go b/internal/httpclient/model_registration_flow_state.go new file mode 100644 index 000000000000..c3be9f33cd79 --- /dev/null +++ b/internal/httpclient/model_registration_flow_state.go @@ -0,0 +1,85 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" +) + +// RegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +type RegistrationFlowState string + +// List of RegistrationFlowState +const ( + REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" + REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" + REGISTRATIONFLOWSTATE_PASSED_CHALLENGE RegistrationFlowState = "passed_challenge" +) + +func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := RegistrationFlowState(value) + for _, existing := range []RegistrationFlowState{"choose_method", "sent_email", "passed_challenge"} { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) +} + +// Ptr returns reference to RegistrationFlowState value +func (v RegistrationFlowState) Ptr() *RegistrationFlowState { + return &v +} + +type NullableRegistrationFlowState struct { + value *RegistrationFlowState + isSet bool +} + +func (v NullableRegistrationFlowState) Get() *RegistrationFlowState { + return v.value +} + +func (v *NullableRegistrationFlowState) Set(val *RegistrationFlowState) { + v.value = val + v.isSet = true +} + +func (v NullableRegistrationFlowState) IsSet() bool { + return v.isSet +} + +func (v *NullableRegistrationFlowState) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRegistrationFlowState(val *RegistrationFlowState) *NullableRegistrationFlowState { + return &NullableRegistrationFlowState{value: val, isSet: true} +} + +func (v NullableRegistrationFlowState) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRegistrationFlowState) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_settings_flow.go b/internal/httpclient/model_settings_flow.go index a1dc0aa98dc6..fa5cd9317c54 100644 --- a/internal/httpclient/model_settings_flow.go +++ b/internal/httpclient/model_settings_flow.go @@ -32,8 +32,9 @@ type SettingsFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl string `json:"request_url"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State SettingsFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -43,7 +44,7 @@ type SettingsFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SettingsFlowState, type_ string, ui UiContainer) *SettingsFlow { +func NewSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state interface{}, type_ string, ui UiContainer) *SettingsFlow { this := SettingsFlow{} this.ExpiresAt = expiresAt this.Id = id @@ -281,9 +282,10 @@ func (o *SettingsFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *SettingsFlow) GetState() SettingsFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *SettingsFlow) GetState() interface{} { if o == nil { - var ret SettingsFlowState + var ret interface{} return ret } @@ -292,15 +294,16 @@ func (o *SettingsFlow) GetState() SettingsFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *SettingsFlow) GetStateOk() (*SettingsFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *SettingsFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *SettingsFlow) SetState(v SettingsFlowState) { +func (o *SettingsFlow) SetState(v interface{}) { o.State = v } @@ -378,7 +381,7 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/httpclient/model_settings_flow_state.go b/internal/httpclient/model_settings_flow_state.go index f994c786a2d8..6d9e5b93f1fe 100644 --- a/internal/httpclient/model_settings_flow_state.go +++ b/internal/httpclient/model_settings_flow_state.go @@ -19,7 +19,7 @@ import ( // SettingsFlowState show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. type SettingsFlowState string -// List of settingsFlowState +// List of SettingsFlowState const ( SETTINGSFLOWSTATE_SHOW_FORM SettingsFlowState = "show_form" SETTINGSFLOWSTATE_SUCCESS SettingsFlowState = "success" @@ -42,7 +42,7 @@ func (v *SettingsFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid SettingsFlowState", value) } -// Ptr returns reference to settingsFlowState value +// Ptr returns reference to SettingsFlowState value func (v SettingsFlowState) Ptr() *SettingsFlowState { return &v } diff --git a/internal/httpclient/model_update_login_flow_with_code_method.go b/internal/httpclient/model_update_login_flow_with_code_method.go new file mode 100644 index 000000000000..bd97ab583ebc --- /dev/null +++ b/internal/httpclient/model_update_login_flow_with_code_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithCodeMethod Update Login flow using the code method +type UpdateLoginFlowWithCodeMethod struct { + // Code is the 6 digits code sent to the user + Code *string `json:"code,omitempty"` + // CSRFToken is the anti-CSRF token + CsrfToken string `json:"csrf_token"` + // Identifier is the code identifier The identifier requires that the user has already completed the registration or settings with code flow. + Identifier *string `json:"identifier,omitempty"` + // Method should be set to \"code\" when logging in using the code strategy. + Method string `json:"method"` + // Resend is set when the user wants to resend the code + Resend *string `json:"resend,omitempty"` +} + +// NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithCodeMethod(csrfToken string, method string) *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + this.CsrfToken = csrfToken + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithCodeMethodWithDefaults instantiates a new UpdateLoginFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMethod { + this := UpdateLoginFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateLoginFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfToken() string { + if o == nil { + var ret string + return ret + } + + return o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CsrfToken, true +} + +// SetCsrfToken sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = v +} + +// GetIdentifier returns the Identifier field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifier() string { + if o == nil || o.Identifier == nil { + var ret string + return ret + } + return *o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetIdentifierOk() (*string, bool) { + if o == nil || o.Identifier == nil { + return nil, false + } + return o.Identifier, true +} + +// HasIdentifier returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasIdentifier() bool { + if o != nil && o.Identifier != nil { + return true + } + + return false +} + +// SetIdentifier gets a reference to the given string and assigns it to the Identifier field. +func (o *UpdateLoginFlowWithCodeMethod) SetIdentifier(v string) { + o.Identifier = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if true { + toSerialize["csrf_token"] = o.CsrfToken + } + if o.Identifier != nil { + toSerialize["identifier"] = o.Identifier + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithCodeMethod struct { + value *UpdateLoginFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithCodeMethod) Get() *UpdateLoginFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Set(val *UpdateLoginFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithCodeMethod(val *UpdateLoginFlowWithCodeMethod) *NullableUpdateLoginFlowWithCodeMethod { + return &NullableUpdateLoginFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_registration_flow_with_code_method.go b/internal/httpclient/model_update_registration_flow_with_code_method.go new file mode 100644 index 000000000000..46b9126d666f --- /dev/null +++ b/internal/httpclient/model_update_registration_flow_with_code_method.go @@ -0,0 +1,286 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithCodeMethod Update Registration Flow with Code Method +type UpdateRegistrationFlowWithCodeMethod struct { + // The OTP Code sent to the user + Code *string `json:"code,omitempty"` + // The CSRF Token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method to use This field must be set to `code` when using the code method. + Method string `json:"method"` + // Resend restarts the flow with a new code + Resend *string `json:"resend,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithCodeMethod instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithCodeMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithCodeMethodWithDefaults instantiates a new UpdateRegistrationFlowWithCodeMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithCodeMethodWithDefaults() *UpdateRegistrationFlowWithCodeMethod { + this := UpdateRegistrationFlowWithCodeMethod{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCode() string { + if o == nil || o.Code == nil { + var ret string + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCodeOk() (*string, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given string and assigns it to the Code field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCode(v string) { + o.Code = &v +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetMethod(v string) { + o.Method = v +} + +// GetResend returns the Resend field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResend() string { + if o == nil || o.Resend == nil { + var ret string + return ret + } + return *o.Resend +} + +// GetResendOk returns a tuple with the Resend field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetResendOk() (*string, bool) { + if o == nil || o.Resend == nil { + return nil, false + } + return o.Resend, true +} + +// HasResend returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasResend() bool { + if o != nil && o.Resend != nil { + return true + } + + return false +} + +// SetResend gets a reference to the given string and assigns it to the Resend field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetResend(v string) { + o.Resend = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithCodeMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Resend != nil { + toSerialize["resend"] = o.Resend + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithCodeMethod struct { + value *UpdateRegistrationFlowWithCodeMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) Get() *UpdateRegistrationFlowWithCodeMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Set(val *UpdateRegistrationFlowWithCodeMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithCodeMethod(val *UpdateRegistrationFlowWithCodeMethod) *NullableUpdateRegistrationFlowWithCodeMethod { + return &NullableUpdateRegistrationFlowWithCodeMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithCodeMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_verification_flow.go b/internal/httpclient/model_verification_flow.go index 5190da660254..c10870c9f841 100644 --- a/internal/httpclient/model_verification_flow.go +++ b/internal/httpclient/model_verification_flow.go @@ -29,8 +29,9 @@ type VerificationFlow struct { // RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL's path or query for example. RequestUrl *string `json:"request_url,omitempty"` // ReturnTo contains the requested return_to URL. - ReturnTo *string `json:"return_to,omitempty"` - State VerificationFlowState `json:"state"` + ReturnTo *string `json:"return_to,omitempty"` + // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. + State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -40,7 +41,7 @@ type VerificationFlow struct { // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewVerificationFlow(id string, state VerificationFlowState, type_ string, ui UiContainer) *VerificationFlow { +func NewVerificationFlow(id string, state interface{}, type_ string, ui UiContainer) *VerificationFlow { this := VerificationFlow{} this.Id = id this.State = state @@ -242,9 +243,10 @@ func (o *VerificationFlow) SetReturnTo(v string) { } // GetState returns the State field value -func (o *VerificationFlow) GetState() VerificationFlowState { +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *VerificationFlow) GetState() interface{} { if o == nil { - var ret VerificationFlowState + var ret interface{} return ret } @@ -253,15 +255,16 @@ func (o *VerificationFlow) GetState() VerificationFlowState { // GetStateOk returns a tuple with the State field value // and a boolean to check if the value has been set. -func (o *VerificationFlow) GetStateOk() (*VerificationFlowState, bool) { - if o == nil { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *VerificationFlow) GetStateOk() (*interface{}, bool) { + if o == nil || o.State == nil { return nil, false } return &o.State, true } // SetState sets field value -func (o *VerificationFlow) SetState(v VerificationFlowState) { +func (o *VerificationFlow) SetState(v interface{}) { o.State = v } @@ -333,7 +336,7 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.ReturnTo != nil { toSerialize["return_to"] = o.ReturnTo } - if true { + if o.State != nil { toSerialize["state"] = o.State } if true { diff --git a/internal/httpclient/model_verification_flow_state.go b/internal/httpclient/model_verification_flow_state.go index bea74568c94d..b34326eec3fc 100644 --- a/internal/httpclient/model_verification_flow_state.go +++ b/internal/httpclient/model_verification_flow_state.go @@ -19,7 +19,7 @@ import ( // VerificationFlowState The state represents the state of the verification flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. type VerificationFlowState string -// List of verificationFlowState +// List of VerificationFlowState const ( VERIFICATIONFLOWSTATE_CHOOSE_METHOD VerificationFlowState = "choose_method" VERIFICATIONFLOWSTATE_SENT_EMAIL VerificationFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *VerificationFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid VerificationFlowState", value) } -// Ptr returns reference to verificationFlowState value +// Ptr returns reference to VerificationFlowState value func (v VerificationFlowState) Ptr() *VerificationFlowState { return &v } diff --git a/internal/testhelpers/courier.go b/internal/testhelpers/courier.go index 825ff4c0ec6d..fd9aa63f45d2 100644 --- a/internal/testhelpers/courier.go +++ b/internal/testhelpers/courier.go @@ -6,25 +6,38 @@ package testhelpers import ( "context" "regexp" + "sort" "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/kratos/courier" + "github.com/ory/x/pagination/keysetpagination" ) -func CourierExpectMessage(t *testing.T, reg interface { +func CourierExpectMessage(ctx context.Context, t *testing.T, reg interface { courier.PersistenceProvider -}, recipient, subject string) *courier.Message { - message, err := reg.CourierPersister().LatestQueuedMessage(context.Background()) +}, recipient, subject string, +) *courier.Message { + messages, total, _, err := reg.CourierPersister().ListMessages(ctx, courier.ListCourierMessagesParameters{ + Recipient: recipient, + }, []keysetpagination.Option{}) require.NoError(t, err) + require.GreaterOrEqual(t, total, int64(1)) - assert.EqualValues(t, subject, strings.TrimSpace(message.Subject)) - assert.EqualValues(t, recipient, strings.TrimSpace(message.Recipient)) + sort.Slice(messages, func(i, j int) bool { + return messages[i].CreatedAt.After(messages[j].CreatedAt) + }) - return message + for _, m := range messages { + if strings.EqualFold(m.Recipient, recipient) && strings.EqualFold(m.Subject, subject) { + return &m + } + } + + require.Failf(t, "could not find courier messages with recipient %s and subject %s", recipient, subject) + return nil } func CourierExpectLinkInMessage(t *testing.T, message *courier.Message, offset int) string { diff --git a/persistence/reference.go b/persistence/reference.go index 215ceb4a7f3f..56a7ca1712df 100644 --- a/persistence/reference.go +++ b/persistence/reference.go @@ -51,6 +51,8 @@ type Persister interface { link.VerificationTokenPersister code.RecoveryCodePersister code.VerificationCodePersister + code.RegistrationCodePersister + code.LoginCodePersister CleanupDatabase(context.Context, time.Duration, time.Duration, int) error Close(context.Context) error diff --git a/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json b/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json new file mode 100644 index 000000000000..bed9cbb51ee4 --- /dev/null +++ b/persistence/sql/migratest/fixtures/identity/28ff0031-190b-4253-bd15-14308dec013e.json @@ -0,0 +1,17 @@ +{ + "id": "28ff0031-190b-4253-bd15-14308dec013e", + "schema_id": "default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", + "state": "active", + "traits": { + "email": "bazbarbarfoo@ory.sh" + }, + "metadata_public": { + "foo": "bar" + }, + "metadata_admin": { + "baz": "bar" + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z" +} diff --git a/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json b/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json new file mode 100644 index 000000000000..e695ce9e3ecf --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_code/bd292366-af32-4ba6-bdf0-11d6d1a217f3.json @@ -0,0 +1,6 @@ +{ + "id": "bd292366-af32-4ba6-bdf0-11d6d1a217f3", + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z", + "identity_id": "28ff0031-190b-4253-bd15-14308dec013e" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json b/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json new file mode 100644 index 000000000000..35690d9d954b --- /dev/null +++ b/persistence/sql/migratest/fixtures/login_flow/00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json @@ -0,0 +1,18 @@ +{ + "id": "00b1517f-2467-4aaf-b0a5-82b4a27dcaf5", + "oauth2_login_challenge": "challenge data", + "type": "api", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "created_at": "2013-10-07T08:23:19Z", + "updated_at": "2013-10-07T08:23:19Z", + "refresh": false, + "requested_aal": "aal1", + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json b/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json index e48e54d97a6b..ce8841aa07ff 100644 --- a/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json +++ b/persistence/sql/migratest/fixtures/login_flow/0bc96cc9-dda4-4700-9e42-35731f2af91e.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json b/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json index 5f63a7ec006a..770f0b2e2c38 100644 --- a/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json +++ b/persistence/sql/migratest/fixtures/login_flow/1fb23c75-b809-42cc-8984-6ca2d0a1192f.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal2" + "requested_aal": "aal2", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json b/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json index efbd0740cdfb..b6cd377d812f 100644 --- a/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json +++ b/persistence/sql/migratest/fixtures/login_flow/202c1981-1e25-47f0-8764-75ad506c2bec.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json b/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json index 7586d19409ab..effdc9f1f2a0 100644 --- a/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json +++ b/persistence/sql/migratest/fixtures/login_flow/349c945a-60f8-436a-a301-7a42c92604f9.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal2" + "requested_aal": "aal2", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json b/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json index 084b36a0c0b9..6eac76a4e91b 100644 --- a/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json +++ b/persistence/sql/migratest/fixtures/login_flow/38caf592-b042-4551-b92f-8d5223c2a4e2.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal2" + "requested_aal": "aal2", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json b/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json index 13dff119fce0..577b054917db 100644 --- a/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json +++ b/persistence/sql/migratest/fixtures/login_flow/3a9ea34f-0f12-469b-9417-3ae5795a7baa.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json b/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json index 5f1529c393b3..6f0fae29f575 100644 --- a/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json +++ b/persistence/sql/migratest/fixtures/login_flow/43c99182-bb67-47e1-b564-bb23bd8d4393.json @@ -13,5 +13,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": true, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json b/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json index fe46265a6d2e..64a415dfba4a 100644 --- a/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json +++ b/persistence/sql/migratest/fixtures/login_flow/47edd3a8-0998-4779-9469-f4b8ee4430df.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json b/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json index 85156c189e4d..e2ccb8f7616d 100644 --- a/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json +++ b/persistence/sql/migratest/fixtures/login_flow/56d94e8b-8a5d-4b7f-8a6e-3259d2b2903e.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json b/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json index c38727386af7..863594687d00 100644 --- a/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json +++ b/persistence/sql/migratest/fixtures/login_flow/6d387820-f2f4-4f9f-9980-a90d89e7811f.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json b/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json index eb8ec21e0e31..138f4838c466 100644 --- a/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json +++ b/persistence/sql/migratest/fixtures/login_flow/916ded11-aa64-4a27-b06e-96e221a509d7.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json b/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json index 418e16ebe69b..41bc0e84748f 100644 --- a/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json +++ b/persistence/sql/migratest/fixtures/login_flow/99974ce6-388c-4669-a95a-7757ee724020.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json b/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json index 84eda2f96615..ae28f38c8fe4 100644 --- a/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json +++ b/persistence/sql/migratest/fixtures/login_flow/b1fac7fb-d016-4a06-a7fe-e4eab2a0429f.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json index b3f93459b975..a2b9861acec7 100644 --- a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json +++ b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af911.json @@ -13,5 +13,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json index 438fb4005e14..e2d58f6dc1fe 100644 --- a/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json +++ b/persistence/sql/migratest/fixtures/login_flow/cccccccc-dda4-4700-9e42-35731f2af91e.json @@ -13,5 +13,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json b/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json index 87ccb1d1dcd0..00a1a2d7c3e5 100644 --- a/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json +++ b/persistence/sql/migratest/fixtures/login_flow/d6aa1f23-88c9-4b9b-a850-392f48c7f9e8.json @@ -12,5 +12,6 @@ "created_at": "2013-10-07T08:23:19Z", "updated_at": "2013-10-07T08:23:19Z", "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json new file mode 100644 index 000000000000..5e429ce3d9ca --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json @@ -0,0 +1,5 @@ +{ + "id": "f1f66a69-ce02-4a12-9591-9e02dda30a0d", + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json b/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json index 1e649d64ad51..ccfcf94814a5 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json +++ b/persistence/sql/migratest/fixtures/registration_flow/05a7f09d-4ef3-41fb-958a-6ad74584b36a.json @@ -8,5 +8,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json b/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json index 7f90a694387d..5c110a3394f5 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json +++ b/persistence/sql/migratest/fixtures/registration_flow/22d58184-b97d-44a5-bbaf-0aa8b4000d81.json @@ -8,5 +8,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json b/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json index dbc832d2aa71..8df52efff06b 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json +++ b/persistence/sql/migratest/fixtures/registration_flow/2bf132e0-5d40-4df9-9a11-9106e5333735.json @@ -8,5 +8,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json b/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json index 6b627d7541f9..d58beb9edffc 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json +++ b/persistence/sql/migratest/fixtures/registration_flow/696e7022-c466-44f6-89c6-8cf93c06a62a.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json new file mode 100644 index 000000000000..e5fcc4a278f1 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json @@ -0,0 +1,14 @@ +{ + "id": "69c80296-36cd-4afc-921a-15369cac5bf0", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json b/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json index 6a1dcdac29dd..19104b6d9f26 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json +++ b/persistence/sql/migratest/fixtures/registration_flow/87fa3f43-5155-42b4-a1ad-174c2595fdaf.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json b/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json index ed2e8512fde1..616af278cd82 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json +++ b/persistence/sql/migratest/fixtures/registration_flow/8ef215a9-e8d5-43b3-9aa3-cb4333562e36.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json b/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json index df3f9c392998..a1f323ba3c4d 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json +++ b/persistence/sql/migratest/fixtures/registration_flow/8f32efdc-f6fc-4c27-a3c2-579d109eff60.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json b/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json index 2195263f1574..1e6cc2579af2 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json +++ b/persistence/sql/migratest/fixtures/registration_flow/9edcf051-1cd0-44cc-bd2f-6ac21f0c24dd.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json b/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json index 497f88de81b2..560741f9a18d 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json +++ b/persistence/sql/migratest/fixtures/registration_flow/e2150cdc-23ac-4940-a240-6c79c27ab029.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json index 8947653d90e7..ce1272433edf 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json +++ b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json index 6763bf5c63f5..4d1d58bdaf51 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json +++ b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be8.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json b/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json index d894073c5468..c7d1b8207a4e 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json +++ b/persistence/sql/migratest/fixtures/registration_flow/f1b5ed18-113a-4a98-aae7-d4eba007199c.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 36126f147935..798afd54dc79 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -73,7 +73,8 @@ func CompareWithFixture(t *testing.T, actual interface{}, prefix string, id stri func TestMigrations_SQLite(t *testing.T) { t.Parallel() sqlite, err := pop.NewConnection(&pop.ConnectionDetails{ - URL: "sqlite3://" + filepath.Join(os.TempDir(), x.NewUUID().String()) + ".sql?_fk=true"}) + URL: "sqlite3://" + filepath.Join(os.TempDir(), x.NewUUID().String()) + ".sql?_fk=true", + }) require.NoError(t, err) require.NoError(t, sqlite.Open()) @@ -105,7 +106,6 @@ func TestMigrations_Cockroach(t *testing.T) { } func testDatabase(t *testing.T, db string, c *pop.Connection) { - ctx := context.Background() l := logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel)) @@ -372,6 +372,40 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_code"), found) }) + t.Run("case=registration_code", func(t *testing.T) { + wg.Add(1) + defer wg.Done() + t.Parallel() + + var ids []code.RegistrationCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "registration_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "registration_code"), found) + }) + + t.Run("case=login_code", func(t *testing.T) { + wg.Add(1) + defer wg.Done() + t.Parallel() + + var ids []code.LoginCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "login_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "login_code"), found) + }) + t.Run("suite=constraints", func(t *testing.T) { // This is not really a parallel test, but we have to mark it parallel so the other tests run first. t.Parallel() diff --git a/persistence/sql/migratest/testdata/20230707133700_testdata.sql b/persistence/sql/migratest/testdata/20230707133700_testdata.sql new file mode 100644 index 000000000000..bcfc9bc12f58 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133700_testdata.sql @@ -0,0 +1,30 @@ +INSERT INTO selfservice_login_flows (id,nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, internal_context, oauth2_login_challenge_data, state) +VALUES ('00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', + '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', '{"foo":"bar"}', 'challenge data', 'choose_method'); + +INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at, metadata_public, metadata_admin, + available_aal) +VALUES ('28ff0031-190b-4253-bd15-14308dec013e', '884f556e-eb3a-4b9f-bee3-11345642c6c0', 'default', + '{"email":"bazbarbarfoo@ory.sh"}', '2013-10-07 08:23:19', '2013-10-07 08:23:19', '{"foo":"bar"}', '{"baz":"bar"}', + NULL); + +INSERT INTO identity_login_codes (id, code, address, address_type, used_at, expires_at, issued_at, selfservice_login_flow_id, identity_id, + created_at, updated_at, nid) +VALUES ('bd292366-af32-4ba6-bdf0-11d6d1a217f3', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +'bazbarbarfoo@ory.com', +'email', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', +'28ff0031-190b-4253-bd15-14308dec013e', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'884f556e-eb3a-4b9f-bee3-11345642c6c0' +) diff --git a/persistence/sql/migratest/testdata/20230707133701_testdata.sql b/persistence/sql/migratest/testdata/20230707133701_testdata.sql new file mode 100644 index 000000000000..8a256314ae95 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133701_testdata.sql @@ -0,0 +1,23 @@ +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, + created_at, updated_at, type, ui, internal_context, oauth2_login_challenge, state) +VALUES ('69c80296-36cd-4afc-921a-15369cac5bf0', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'browser', '{}', '{"foo":"bar"}', + '3caddfd5-9903-4bce-83ff-cae36f42dff7', 'choose_method'); + +INSERT INTO identity_registration_codes (id, address, address_type, code, used_at, expires_at, issued_at, selfservice_registration_flow_id, + created_at, updated_at, nid) +VALUES ('f1f66a69-ce02-4a12-9591-9e02dda30a0d', +'example@example.com', +'email', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'69c80296-36cd-4afc-921a-15369cac5bf0', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'884f556e-eb3a-4b9f-bee3-11345642c6c0' +) diff --git a/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql new file mode 100644 index 000000000000..ddd3c7bbfbc0 --- /dev/null +++ b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.down.sql @@ -0,0 +1,2 @@ +ALTER table selfservice_registration_flows DROP COLUMN state; +ALTER table selfservice_login_flows DROP COLUMN state; diff --git a/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql new file mode 100644 index 000000000000..26f4d0649508 --- /dev/null +++ b/persistence/sql/migrations/sql/20230703143600000001_selfservice_registration_login_flows_state.up.sql @@ -0,0 +1,2 @@ +ALTER table selfservice_login_flows ADD state VARCHAR(255) NULL; +ALTER table selfservice_registration_flows ADD state VARCHAR(255) NULL; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql new file mode 100644 index 000000000000..79a48193bfe8 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql @@ -0,0 +1,4 @@ +DROP TABLE identity_login_codes; + +ALTER TABLE selfservice_login_flows DROP submit_count; +ALTER TABLE selfservice_login_flows DROP skip_csrf_check; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql new file mode 100644 index 000000000000..cdb888626d54 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE identity_login_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id CHAR(36), + identity_id CHAR(36) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_login_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql new file mode 100644 index 000000000000..448dd5e0c257 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql @@ -0,0 +1,29 @@ +CREATE TABLE identity_login_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id UUID NOT NULL, + identity_id UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid UUID NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_login_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql new file mode 100644 index 000000000000..cca834d74de3 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql @@ -0,0 +1,4 @@ +DROP TABLE identity_registration_codes; + +ALTER TABLE selfservice_registration_flows DROP submit_count; +ALTER TABLE selfservice_registration_flows DROP skip_csrf_check; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql new file mode 100644 index 000000000000..6a5e20cdfb81 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE identity_registration_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_registration_flow_id CHAR(36), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_registration_codes_selfservice_registration_flows_id_fk + FOREIGN KEY (selfservice_registration_flow_id) + REFERENCES selfservice_registration_flows (id) + ON DELETE cascade, + CONSTRAINT identity_registration_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid, selfservice_registration_flow_id); +CREATE INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes (id, nid); + +ALTER TABLE selfservice_registration_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_registration_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql new file mode 100644 index 000000000000..9ac21a49e60f --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE identity_registration_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR(64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, + address_type CHAR(36) NOT NULL, + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_registration_flow_id UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid UUID NOT NULL, + CONSTRAINT identity_registration_codes_selfservice_registration_flows_id_fk + FOREIGN KEY (selfservice_registration_flow_id) + REFERENCES selfservice_registration_flows (id) + ON DELETE cascade, + CONSTRAINT identity_registration_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registration_codes (nid, selfservice_registration_flow_id); +CREATE INDEX identity_registration_codes_id_nid_idx ON identity_registration_codes (id, nid); + +ALTER TABLE selfservice_registration_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_registration_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql new file mode 100644 index 000000000000..84f10f939a12 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'code'; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql new file mode 100644 index 000000000000..47e0cf0b2b34 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '14f3b7e2-8725-4068-be39-8a796485fd97', 'code' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'code'); diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go index 1f29a1860e35..2982216abec5 100644 --- a/persistence/sql/persister_login.go +++ b/persistence/sql/persister_login.go @@ -5,17 +5,21 @@ package sql import ( "context" + "crypto/subtle" "fmt" "time" "github.com/gobuffalo/pop/v6" + "github.com/pkg/errors" "github.com/gofrs/uuid" "github.com/ory/x/sqlcon" "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" ) var _ login.FlowPersister = new(Persister) @@ -84,3 +88,123 @@ func (p *Persister) DeleteExpiredLoginFlows(ctx context.Context, expiresAt time. } return nil } + +func (p *Persister) CreateLoginCode(ctx context.Context, codeParams *code.CreateLoginCodeParams) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginCode") + defer span.End() + + now := time.Now().UTC() + loginCode := &code.LoginCode{ + IdentityID: codeParams.IdentityID, + Address: codeParams.Address, + AddressType: codeParams.AddressType, + CodeHMAC: p.hmacValue(ctx, codeParams.RawCode), + IssuedAt: now, + ExpiresAt: now.Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: codeParams.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return loginCode, nil +} + +func (p *Persister) UseLoginCode(ctx context.Context, flowID uuid.UUID, identityID uuid.UUID, codeVal string) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseLoginCode") + defer span.End() + + var loginCode *code.LoginCode + + nid := p.NetworkID(ctx) + flowTableName := new(login.Flow).TableName(ctx) + + if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { + //#nosec G201 -- TableName is static + if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), flowID, nid).Exec()); err != nil { + return err + } + + var submitCount int + // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. + //#nosec G201 -- TableName is static + if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), flowID, nid).First(&submitCount)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction + return nil + } + return err + } + + if submitCount > 5 { + return errors.WithStack(code.ErrCodeSubmittedTooOften) + } + + var loginCodes []code.LoginCode + if err = sqlcon.HandleError(tx.Where("nid = ? AND selfservice_login_flow_id = ? AND identity_id = ?", nid, flowID, identityID).All(&loginCodes)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + return err + } + return nil + } + + secrets: + for _, secret := range p.r.Config().SecretsSession(ctx) { + suppliedCode := []byte(p.hmacValueWithSecret(ctx, codeVal, secret)) + for i := range loginCodes { + code := loginCodes[i] + if subtle.ConstantTimeCompare([]byte(code.CodeHMAC), suppliedCode) == 0 { + // Not the supplied code + continue + } + loginCode = &code + break secrets + } + } + + if loginCode == nil || !loginCode.IsValid() { + // Return no error, as that would roll back the transaction + return nil + } + + //#nosec G201 -- TableName is static + return sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", loginCode.TableName(ctx)), time.Now().UTC(), loginCode.ID, nid).Exec()) + })); err != nil { + return nil, err + } + + if loginCode == nil { + return nil, errors.WithStack(code.ErrCodeNotFound) + } + + if loginCode.IsExpired() { + return nil, errors.WithStack(flow.NewFlowExpiredError(loginCode.ExpiresAt)) + } + + if loginCode.WasUsed() { + return nil, errors.WithStack(code.ErrCodeAlreadyUsed) + } + + return loginCode, nil +} + +func (p *Persister) DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteLoginCodesOfFlow") + defer span.End() + + //#nosec G201 -- TableName is static + return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE selfservice_login_flow_id = ? AND nid = ?", new(code.LoginCode).TableName(ctx)), flowID, p.NetworkID(ctx)).Exec() +} + +func (p *Persister) GetUsedLoginCode(ctx context.Context, flowID uuid.UUID) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedLoginCode") + defer span.End() + + var loginCode code.LoginCode + if err := p.Connection(ctx).RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_login_flow_id = ? AND nid = ? AND used_at IS NOT NULL", new(code.LoginCode).TableName(ctx)), flowID, p.NetworkID(ctx)).First(&loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return &loginCode, nil +} diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go index d34a6fabd435..34539832d254 100644 --- a/persistence/sql/persister_recovery.go +++ b/persistence/sql/persister_recovery.go @@ -23,8 +23,10 @@ import ( "github.com/ory/x/sqlcon" ) -var _ recovery.FlowPersister = new(Persister) -var _ link.RecoveryTokenPersister = new(Persister) +var ( + _ recovery.FlowPersister = new(Persister) + _ link.RecoveryTokenPersister = new(Persister) +) func (p *Persister) CreateRecoveryFlow(ctx context.Context, r *recovery.Flow) error { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryFlow") @@ -186,7 +188,6 @@ func (p *Persister) UseRecoveryCode(ctx context.Context, fID uuid.UUID, codeVal flowTableName := new(recovery.Flow).TableName(ctx) if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { - //#nosec G201 -- TableName is static if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), fID, nid).Exec()); err != nil { return err @@ -256,15 +257,15 @@ func (p *Persister) UseRecoveryCode(ctx context.Context, fID uuid.UUID, codeVal } if recoveryCode == nil { - return nil, code.ErrCodeNotFound + return nil, errors.WithStack(code.ErrCodeNotFound) } if recoveryCode.IsExpired() { - return nil, flow.NewFlowExpiredError(recoveryCode.ExpiresAt) + return nil, errors.WithStack(flow.NewFlowExpiredError(recoveryCode.ExpiresAt)) } if recoveryCode.WasUsed() { - return nil, code.ErrCodeAlreadyUsed + return nil, errors.WithStack(code.ErrCodeAlreadyUsed) } return recoveryCode, nil diff --git a/persistence/sql/persister_registration.go b/persistence/sql/persister_registration.go index fe7e25ceeac3..7bd665fcedf0 100644 --- a/persistence/sql/persister_registration.go +++ b/persistence/sql/persister_registration.go @@ -5,15 +5,21 @@ package sql import ( "context" + "crypto/subtle" "fmt" "time" + "github.com/bxcodec/faker/v3/support/slice" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" + "github.com/pkg/errors" "github.com/ory/x/sqlcon" "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" ) func (p *Persister) CreateRegistrationFlow(ctx context.Context, r *registration.Flow) error { @@ -64,3 +70,133 @@ func (p *Persister) DeleteExpiredRegistrationFlows(ctx context.Context, expiresA } return nil } + +func (p *Persister) CreateRegistrationCode(ctx context.Context, codeParams *code.CreateRegistrationCodeParams) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRegistrationCode") + defer span.End() + + now := time.Now() + + registrationCode := &code.RegistrationCode{ + Address: codeParams.Address, + AddressType: codeParams.AddressType, + CodeHMAC: p.hmacValue(ctx, codeParams.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: codeParams.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(registrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return registrationCode, nil +} + +func (p *Persister) UseRegistrationCode(ctx context.Context, flowID uuid.UUID, rawCode string, addresses ...string) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRegistrationCode") + defer span.End() + + nid := p.NetworkID(ctx) + + flowTableName := new(registration.Flow).TableName(ctx) + + var registrationCode *code.RegistrationCode + if err := sqlcon.HandleError(p.GetConnection(ctx).Transaction(func(tx *pop.Connection) error { + if err := tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), flowID, nid).Exec(); err != nil { + return err + } + + var submitCount int + // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. + //#nosec G201 -- TableName is static + if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), flowID, nid).First(&submitCount)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction + return nil + } + return err + } + + // This check prevents parallel brute force attacks to generate the recovery code + // by checking the submit count inside this database transaction. + // If the flow has been submitted more than 5 times, the transaction is aborted (regardless of whether the code was correct or not) + // and we thus give no indication whether the supplied code was correct or not. See also https://github.com/ory/kratos/pull/2645#discussion_r984732899 + if submitCount > 5 { + return errors.WithStack(code.ErrCodeSubmittedTooOften) + } + + var registrationCodes []code.RegistrationCode + if err := sqlcon.HandleError(tx.Where("nid = ? AND selfservice_registration_flow_id = ?", nid, flowID).All(®istrationCodes)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction + return nil + } + + return err + } + + secrets: + for _, secret := range p.r.Config().SecretsSession(ctx) { + suppliedCode := []byte(p.hmacValueWithSecret(ctx, rawCode, secret)) + for i := range registrationCodes { + code := registrationCodes[i] + if subtle.ConstantTimeCompare([]byte(code.CodeHMAC), suppliedCode) == 0 { + // Not the supplied code + continue + } + registrationCode = &code + break secrets + } + } + + if registrationCode == nil || !registrationCode.IsValid() { + // Return no error, as that would roll back the transaction + return nil + } + + //#nosec G201 -- TableName is static + return sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", registrationCode.TableName(ctx)), time.Now().UTC(), registrationCode.ID, nid).Exec()) + })); err != nil { + return nil, err + } + + if registrationCode == nil { + return nil, errors.WithStack(code.ErrCodeNotFound) + } + + if registrationCode.IsExpired() { + return nil, errors.WithStack(flow.NewFlowExpiredError(registrationCode.ExpiresAt)) + } + + if registrationCode.WasUsed() { + return nil, errors.WithStack(code.ErrCodeAlreadyUsed) + } + + // ensure that the identifiers extracted from the traits are contained in the registration code + if !slice.Contains(addresses, registrationCode.Address) { + return nil, errors.WithStack(code.ErrCodeNotFound) + } + + return registrationCode, nil +} + +func (p *Persister) DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRegistrationCodesOfFlow") + defer span.End() + + //#nosec G201 -- TableName is static + return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE selfservice_registration_flow_id = ? AND nid = ?", new(code.RegistrationCode).TableName(ctx)), flowID, p.NetworkID(ctx)).Exec() +} + +func (p *Persister) GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedRegistrationCode") + defer span.End() + + var registrationCode code.RegistrationCode + if err := p.Connection(ctx).RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ? AND used_at IS NOT NULL AND nid = ?", new(code.RegistrationCode).TableName(ctx)), flowID, p.NetworkID(ctx)).First(®istrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return ®istrationCode, nil +} diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go index 30bc4ac56718..9892f41e9fc0 100644 --- a/persistence/sql/persister_verification.go +++ b/persistence/sql/persister_verification.go @@ -237,7 +237,7 @@ func (p *Persister) UseVerificationCode(ctx context.Context, fID uuid.UUID, code } if verificationCode == nil { - return nil, code.ErrCodeNotFound + return nil, errors.WithStack(code.ErrCodeNotFound) } return verificationCode, nil diff --git a/schema/errors.go b/schema/errors.go index 68683d369a06..6c2f20a411b5 100644 --- a/schema/errors.go +++ b/schema/errors.go @@ -346,3 +346,43 @@ func NewNoWebAuthnCredentials() error { Messages: new(text.Messages).Add(text.NewErrorValidationSuchNoWebAuthnUser()), }) } + +func NewNoCodeAuthnCredentials() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `account does not exist or has not setup up sign in with code`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationNoCodeUser()), + }) +} + +func NewTraitsMismatch() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the submitted form data has changed from the previous submission. Please try again.`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationTraitsMismatch()), + }) +} + +func NewRegistrationCodeInvalid() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided code is invalid or has already been used. Please try again.`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed()), + }) +} + +func NewLoginCodeInvalid() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `the provided code is invalid or has already been used. Please try again.`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed()), + }) +} diff --git a/schema/extension.go b/schema/extension.go index 5955328c27df..5b605cfb91d4 100644 --- a/schema/extension.go +++ b/schema/extension.go @@ -30,6 +30,10 @@ type ( TOTP struct { AccountName bool `json:"account_name"` } `json:"totp"` + Code struct { + Identifier bool `json:"identifier"` + Via string `json:"via"` + } `json:"code"` } `json:"credentials"` Verification struct { Via string `json:"via"` diff --git a/selfservice/flow/error_test.go b/selfservice/flow/error_test.go index 6502244fba69..98b1ad32e9c4 100644 --- a/selfservice/flow/error_test.go +++ b/selfservice/flow/error_test.go @@ -52,6 +52,17 @@ type testFlow struct { // // required: true UI *container.Container `json:"ui" db:"ui"` + + // Flow State + // + // The state represents the state of the verification flow. + // + // - choose_method: ask the user to choose a method (e.g. recover account via email) + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the recovery challenge was passed. + // + // required: true + State State `json:"state" db:"state"` } func (t *testFlow) GetID() uuid.UUID { @@ -74,6 +85,18 @@ func (t *testFlow) GetUI() *container.Container { return t.UI } +func (t *testFlow) GetState() State { + return t.State +} + +func (t *testFlow) GetFlowName() FlowName { + return FlowName("test") +} + +func (t *testFlow) SetState(state State) { + t.State = state +} + func newTestFlow(r *http.Request, flowType Type) Flow { id := x.NewUUID() requestURL := x.RequestURL(r).String() diff --git a/selfservice/flow/flow.go b/selfservice/flow/flow.go index 912cb72fa715..641e9ca30363 100644 --- a/selfservice/flow/flow.go +++ b/selfservice/flow/flow.go @@ -40,6 +40,9 @@ type Flow interface { GetRequestURL() string AppendTo(*url.URL) *url.URL GetUI() *container.Container + GetState() State + SetState(State) + GetFlowName() FlowName } type Challenger interface { diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index a0276175e2e4..e46698290819 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -124,8 +124,19 @@ type Flow struct { // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", // and only on creating the login flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // State represents the state of this request: + // + // - choose_method: ask the user to choose a method to sign in with + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the login challenge was passed. + // + // required: true + State State `json:"state" faker:"-" db:"state"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, flowType flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -164,6 +175,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques r.URL.Query().Get("aal"), string(identity.AuthenticatorAssuranceLevel1)))), InternalContext: []byte("{}"), + State: flow.StateChooseMethod, }, nil } @@ -251,3 +263,15 @@ func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (o x.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowLoginReturnTo(ctx, f.Active.String())), } } + +func (f *Flow) GetState() flow.State { + return flow.State(f.State) +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.LoginFlow +} + +func (f *Flow) SetState(state flow.State) { + f.State = State(state) +} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 3cf333c4691f..fd300f437587 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -142,7 +142,6 @@ func (e *HookExecutor) PostLoginHook( x.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())), x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(r.Context(), a.Active.String())), ) - if err != nil { return err } diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go index 74160b8d2566..9f1a144ffc2d 100644 --- a/selfservice/flow/login/sort.go +++ b/selfservice/flow/login/sort.go @@ -15,6 +15,7 @@ func sortNodes(ctx context.Context, n node.Nodes) error { node.OpenIDConnectGroup, node.DefaultGroup, node.WebAuthnGroup, + node.CodeGroup, node.PasswordGroup, node.TOTPGroup, node.LookupGroup, diff --git a/selfservice/flow/login/state.go b/selfservice/flow/login/state.go new file mode 100644 index 000000000000..576fad6d9f05 --- /dev/null +++ b/selfservice/flow/login/state.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import "github.com/ory/kratos/selfservice/flow" + +// Login Flow State +// +// The state represents the state of the login flow. +// +// - choose_method: ask the user to choose a method (e.g. login account via email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the login challenge was passed. +// +// swagger:model loginFlowState +type State = flow.State diff --git a/selfservice/flow/name.go b/selfservice/flow/name.go new file mode 100644 index 000000000000..1b766c6662f6 --- /dev/null +++ b/selfservice/flow/name.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +// FlowName is the flow name. +// +// The flow name can be one of: +// - 'login' +// - 'registration' +// - 'settings' +// - 'recovery' +// - 'verification' +// +// swagger:ignore +type FlowName string + +const ( + LoginFlow FlowName = "login" + RegistrationFlow FlowName = "registration" + SettingsFlow FlowName = "settings" + RecoveryFlow FlowName = "recovery" + VerificationFlow FlowName = "verification" +) + +func (t Type) String() string { + return string(t) +} diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go index c6cfc3dfa4c9..3557c8652b8d 100644 --- a/selfservice/flow/recovery/flow.go +++ b/selfservice/flow/recovery/flow.go @@ -102,6 +102,8 @@ type Flow struct { DangerousSkipCSRFCheck bool `json:"-" faker:"-" db:"skip_csrf_check"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, strategy Strategy, ft flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -127,7 +129,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques Method: "POST", Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), }, - State: StateChooseMethod, + State: flow.StateChooseMethod, CSRFToken: csrf, Type: ft, } @@ -222,3 +224,15 @@ func (f *Flow) AfterSave(*pop.Connection) error { func (f *Flow) GetUI() *container.Container { return f.UI } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.RecoveryFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/recovery/flow_test.go b/selfservice/flow/recovery/flow_test.go index c2a1eb56e11d..cab497c1b9a2 100644 --- a/selfservice/flow/recovery/flow_test.go +++ b/selfservice/flow/recovery/flow_test.go @@ -54,7 +54,7 @@ func TestFlow(t *testing.T) { }) } - assert.EqualValues(t, recovery.StateChooseMethod, + assert.EqualValues(t, flow.StateChooseMethod, must(recovery.NewFlow(conf, time.Hour, "", u, nil, flow.TypeBrowser)).State) t.Run("type=return_to", func(t *testing.T) { diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go index 90a62aab829c..6c4fd21344a4 100644 --- a/selfservice/flow/recovery/handler.go +++ b/selfservice/flow/recovery/handler.go @@ -185,7 +185,6 @@ type createBrowserRecoveryFlow struct { // 400: errorGeneric // default: errorGeneric func (h *Handler) createBrowserRecoveryFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - if !h.d.Config().SelfServiceFlowRecoveryEnabled(r.Context()) { h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Recovery is not allowed because it was disabled."))) return diff --git a/selfservice/flow/recovery/state.go b/selfservice/flow/recovery/state.go index 6b2ed0892b06..96ab9d29937e 100644 --- a/selfservice/flow/recovery/state.go +++ b/selfservice/flow/recovery/state.go @@ -3,6 +3,8 @@ package recovery +import "github.com/ory/kratos/selfservice/flow" + // Recovery Flow State // // The state represents the state of the recovery flow. @@ -12,33 +14,4 @@ package recovery // - passed_challenge: the request was successful and the recovery challenge was passed. // // swagger:model recoveryFlowState -type State string - -const ( - StateChooseMethod State = "choose_method" - StateEmailSent State = "sent_email" - StatePassedChallenge State = "passed_challenge" -) - -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} - -func indexOf(current State) int { - for k, s := range states { - if s == current { - return k - } - } - return 0 -} - -func HasReachedState(expected, actual State) bool { - return indexOf(actual) >= indexOf(expected) -} - -func NextState(current State) State { - if current == StatePassedChallenge { - return StatePassedChallenge - } - - return states[indexOf(current)+1] -} +type State = flow.State diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index 0b1c49c69080..07030520c751 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -115,8 +115,18 @@ type Flow struct { // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", // and only on creating the flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // State represents the state of this request: + // + // - choose_method: ask the user to choose a method (e.g. registration with email) + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the registration challenge was passed. + // required: true + State State `json:"state" faker:"-" db:"state"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, ft flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -151,6 +161,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques CSRFToken: csrf, Type: ft, InternalContext: []byte("{}"), + State: flow.StateChooseMethod, }, nil } @@ -242,3 +253,15 @@ func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (o x.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowRegistrationReturnTo(ctx, f.Active.String())), } } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.RegistrationFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/registration/sort.go b/selfservice/flow/registration/sort.go index db44e96274e3..15348dd56512 100644 --- a/selfservice/flow/registration/sort.go +++ b/selfservice/flow/registration/sort.go @@ -16,6 +16,7 @@ func SortNodes(ctx context.Context, n node.Nodes, schemaRef string) error { node.DefaultGroup, node.OpenIDConnectGroup, node.WebAuthnGroup, + node.CodeGroup, node.PasswordGroup, }), node.SortUpdateOrder(node.PasswordLoginOrder), diff --git a/selfservice/flow/registration/state.go b/selfservice/flow/registration/state.go new file mode 100644 index 000000000000..be50551d6850 --- /dev/null +++ b/selfservice/flow/registration/state.go @@ -0,0 +1,15 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import "github.com/ory/kratos/selfservice/flow" + +// State represents the state of this request: +// +// - choose_method: ask the user to choose a method (e.g. registration with email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the registration challenge was passed. +// +// swagger:model registrationFlowState +type State = flow.State diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go index af1b31968caa..1a3b091ed548 100644 --- a/selfservice/flow/request.go +++ b/selfservice/flow/request.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/strategy" "github.com/ory/x/decoderx" @@ -25,6 +26,7 @@ var methodSchema []byte var ErrOriginHeaderNeedsBrowserFlow = herodot.ErrBadRequest. WithReasonf(`The HTTP Request Header included the "Origin" key, indicating that this request was made as part of an AJAX request in a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) + var ErrCookieHeaderNeedsBrowserFlow = herodot.ErrBadRequest. WithReasonf(`The HTTP Request Header included the "Cookie" key, indicating that this request was made by a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) @@ -76,9 +78,10 @@ func EnsureCSRF(reg interface { var dec = decoderx.NewHTTP() -func MethodEnabledAndAllowedFromRequest(r *http.Request, expected string, d interface { +func MethodEnabledAndAllowedFromRequest(r *http.Request, flow FlowName, expected string, d interface { config.Provider -}) error { +}, +) error { var method struct { Method string `json:"method" form:"method"` } @@ -96,17 +99,32 @@ func MethodEnabledAndAllowedFromRequest(r *http.Request, expected string, d inte return errors.WithStack(err) } - return MethodEnabledAndAllowed(r.Context(), expected, method.Method, d) + return MethodEnabledAndAllowed(r.Context(), flow, expected, method.Method, d) } -func MethodEnabledAndAllowed(ctx context.Context, expected, actual string, d interface { +func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, actual string, d interface { config.Provider -}) error { +}, +) error { if actual != expected { return errors.WithStack(ErrStrategyNotResponsible) } - if !d.Config().SelfServiceStrategy(ctx, expected).Enabled { + var ok bool + if strings.EqualFold(actual, identity.CredentialsTypeCodeAuth.String()) { + switch flowName { + case RegistrationFlow: + ok = d.Config().SelfServiceCodeStrategy(ctx).RegistrationEnabled + case LoginFlow: + ok = d.Config().SelfServiceCodeStrategy(ctx).LoginEnabled + default: + ok = false + } + } else { + ok = d.Config().SelfServiceStrategy(ctx, expected).Enabled + } + + if !ok { return errors.WithStack(herodot.ErrNotFound.WithReason(strategy.EndpointDisabledMessage)) } diff --git a/selfservice/flow/request_test.go b/selfservice/flow/request_test.go index d240f9c15daf..4fa39a61bc46 100644 --- a/selfservice/flow/request_test.go +++ b/selfservice/flow/request_test.go @@ -55,7 +55,7 @@ func TestMethodEnabledAndAllowed(t *testing.T) { ctx := context.Background() conf, d := internal.NewFastRegistryWithMocks(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if err := flow.MethodEnabledAndAllowedFromRequest(r, "password", d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, flow.LoginFlow, "password", d); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -91,3 +91,75 @@ func TestMethodEnabledAndAllowed(t *testing.T) { assert.Contains(t, string(body), "The requested resource could not be found") }) } + +func TestMethodCodeEnabledAndAllowed(t *testing.T) { + ctx := context.Background() + conf, d := internal.NewFastRegistryWithMocks(t) + + currentFlow := make(chan flow.FlowName, 1) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f := <-currentFlow + if err := flow.MethodEnabledAndAllowedFromRequest(r, f, "code", d); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + })) + + t.Run("login code allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.login_enabled", true) + currentFlow <- flow.LoginFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + }) + + t.Run("login code not allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.login_enabled", false) + currentFlow <- flow.LoginFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "The requested resource could not be found") + }) + + t.Run("registration code allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.registration_enabled", true) + currentFlow <- flow.RegistrationFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + }) + + t.Run("registration code not allowed", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.registration_enabled", false) + currentFlow <- flow.RegistrationFlow + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "The requested resource could not be found") + }) + + t.Run("recovery and verification should still be allowed if registration and login is disabled", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.registration_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.login_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + + for _, f := range []flow.FlowName{flow.RecoveryFlow, flow.VerificationFlow} { + currentFlow <- f + res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNoContent, res.StatusCode) + } + }) +} diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go index ef69a03f042e..a96da053d766 100644 --- a/selfservice/flow/settings/flow.go +++ b/selfservice/flow/settings/flow.go @@ -121,6 +121,8 @@ type Flow struct { ContinueWithItems []flow.ContinueWith `json:"continue_with,omitempty" db:"-" faker:"-" ` } +var _ flow.Flow = new(Flow) + func MustNewFlow(conf *config.Config, exp time.Duration, r *http.Request, i *identity.Identity, ft flow.Type) *Flow { f, err := NewFlow(conf, exp, r, i, ft) if err != nil { @@ -153,7 +155,7 @@ func NewFlow(conf *config.Config, exp time.Duration, r *http.Request, i *identit IdentityID: i.ID, Identity: i, Type: ft, - State: StateShowForm, + State: flow.StateShowForm, UI: &container.Container{ Method: "POST", Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), @@ -242,3 +244,15 @@ func (f *Flow) AddContinueWith(c flow.ContinueWith) { func (f *Flow) ContinueWith() []flow.ContinueWith { return f.ContinueWithItems } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.SettingsFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go index 91c6b2c5122c..c5e35610c7a2 100644 --- a/selfservice/flow/settings/hook.go +++ b/selfservice/flow/settings/hook.go @@ -231,7 +231,7 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, Debug("An identity's settings have been updated.") ctxUpdate.UpdateIdentity(i) - ctxUpdate.Flow.State = StateSuccess + ctxUpdate.Flow.State = flow.StateSuccess if hookOptions.cb != nil { if err := hookOptions.cb(ctxUpdate); err != nil { return err diff --git a/selfservice/flow/settings/state.go b/selfservice/flow/settings/state.go index b605cf7569d8..21cd22f303cc 100644 --- a/selfservice/flow/settings/state.go +++ b/selfservice/flow/settings/state.go @@ -3,6 +3,8 @@ package settings +import "github.com/ory/kratos/selfservice/flow" + // State represents the state of this flow. It knows two states: // // - show_form: No user data has been collected, or it is invalid, and thus the form should be shown. @@ -11,9 +13,4 @@ package settings // when a flow with invalid (e.g. "please use a valid phone number") data was sent. // // swagger:model settingsFlowState -type State string - -const ( - StateShowForm State = "show_form" - StateSuccess State = "success" -) +type State = flow.State diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go new file mode 100644 index 000000000000..3ef8ed5dbe03 --- /dev/null +++ b/selfservice/flow/state.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +// Flow State +// +// The state represents the state of the verification flow. +// +// - choose_method: ask the user to choose a method (e.g. recover account via email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the recovery challenge was passed. +// - show_form: a form is shown to the user to perform the flow +// - success: the flow has been completed successfully +// +// swagger:enum selfServiceFlowState +type State string + +// #nosec G101 -- only a key constant +const ( + StateChooseMethod State = "choose_method" + StateEmailSent State = "sent_email" + StatePassedChallenge State = "passed_challenge" + StateShowForm State = "show_form" + StateSuccess State = "success" +) + +var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} + +func indexOf(current State) int { + for k, s := range states { + if s == current { + return k + } + } + return 0 +} + +func HasReachedState(expected, actual State) bool { + return indexOf(actual) >= indexOf(expected) +} + +func NextState(current State) State { + if current == StatePassedChallenge { + return StatePassedChallenge + } + + return states[indexOf(current)+1] +} diff --git a/selfservice/flow/recovery/state_test.go b/selfservice/flow/state_test.go similarity index 97% rename from selfservice/flow/recovery/state_test.go rename to selfservice/flow/state_test.go index 160be6e74555..349e0425ed44 100644 --- a/selfservice/flow/recovery/state_test.go +++ b/selfservice/flow/state_test.go @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -package recovery +package flow import ( "testing" diff --git a/selfservice/flow/verification/error.go b/selfservice/flow/verification/error.go index fd7542415fb1..b39875b233d0 100644 --- a/selfservice/flow/verification/error.go +++ b/selfservice/flow/verification/error.go @@ -26,9 +26,7 @@ import ( "github.com/ory/kratos/x" ) -var ( - ErrHookAbortFlow = errors.New("aborted verification hook execution") -) +var ErrHookAbortFlow = errors.New("aborted verification hook execution") type ( errorHandlerDependencies interface { diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go index 32243cc1bed6..8d12db8fcdfe 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -91,6 +91,8 @@ type Flow struct { NID uuid.UUID `json:"-" faker:"-" db:"nid"` } +var _ flow.Flow = new(Flow) + func (f *Flow) GetType() flow.Type { return f.Type } @@ -129,7 +131,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(), }, CSRFToken: csrf, - State: StateChooseMethod, + State: flow.StateChooseMethod, Type: ft, } @@ -255,3 +257,15 @@ func (f *Flow) ContinueURL(ctx context.Context, config *config.Config) *url.URL } return returnTo } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.VerificationFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/verification/flow_test.go b/selfservice/flow/verification/flow_test.go index fb21b707e387..3485f3454fc4 100644 --- a/selfservice/flow/verification/flow_test.go +++ b/selfservice/flow/verification/flow_test.go @@ -64,7 +64,7 @@ func TestFlow(t *testing.T) { require.NoError(t, err) }) - assert.EqualValues(t, verification.StateChooseMethod, + assert.EqualValues(t, flow.StateChooseMethod, must(verification.NewFlow(conf, time.Hour, "", u, nil, flow.TypeBrowser)).State) } @@ -207,5 +207,4 @@ func TestContinueURL(t *testing.T) { require.Equal(t, tc.expect, url.String()) }) } - } diff --git a/selfservice/flow/verification/state.go b/selfservice/flow/verification/state.go index 9f11037cfeef..84aded971389 100644 --- a/selfservice/flow/verification/state.go +++ b/selfservice/flow/verification/state.go @@ -3,6 +3,8 @@ package verification +import "github.com/ory/kratos/selfservice/flow" + // Verification Flow State // // The state represents the state of the verification flow. @@ -12,33 +14,4 @@ package verification // - passed_challenge: the request was successful and the recovery challenge was passed. // // swagger:model verificationFlowState -type State string - -const ( - StateChooseMethod State = "choose_method" - StateEmailSent State = "sent_email" - StatePassedChallenge State = "passed_challenge" -) - -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} - -func indexOf(current State) int { - for k, s := range states { - if s == current { - return k - } - } - return 0 -} - -func HasReachedState(expected, actual State) bool { - return indexOf(actual) >= indexOf(expected) -} - -func NextState(current State) State { - if current == StatePassedChallenge { - return StatePassedChallenge - } - - return states[indexOf(current)+1] -} +type State = flow.State diff --git a/selfservice/flow/verification/state_test.go b/selfservice/flow/verification/state_test.go deleted file mode 100644 index ab192d4db878..000000000000 --- a/selfservice/flow/verification/state_test.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package verification - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestState(t *testing.T) { - assert.EqualValues(t, StateEmailSent, NextState(StateChooseMethod)) - assert.EqualValues(t, StatePassedChallenge, NextState(StateEmailSent)) - assert.EqualValues(t, StatePassedChallenge, NextState(StatePassedChallenge)) - - assert.True(t, HasReachedState(StatePassedChallenge, StatePassedChallenge)) - assert.False(t, HasReachedState(StatePassedChallenge, StateEmailSent)) - assert.False(t, HasReachedState(StateEmailSent, StateChooseMethod)) -} diff --git a/selfservice/hook/code_address_verifier.go b/selfservice/hook/code_address_verifier.go new file mode 100644 index 000000000000..3c77f3df4ae5 --- /dev/null +++ b/selfservice/hook/code_address_verifier.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "net/http" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +type ( + codeAddressDependencies interface { + config.Provider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + verification.StrategyProvider + verification.FlowPersistenceProvider + code.RegistrationCodePersistenceProvider + identity.PrivilegedPoolProvider + x.WriterProvider + } + CodeAddressVerifier struct { + r codeAddressDependencies + } +) + +var ( + _ registration.PostHookPostPersistExecutor = new(CodeAddressVerifier) +) + +func NewCodeAddressVerifier(r codeAddressDependencies) *CodeAddressVerifier { + return &CodeAddressVerifier{r: r} +} + +func (cv *CodeAddressVerifier) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, s *session.Session) error { + if a.Active != identity.CredentialsTypeCodeAuth { + return nil + } + + recoveryCode, err := cv.r.RegistrationCodePersister().GetUsedRegistrationCode(r.Context(), a.GetID()) + if err != nil { + return err + } + + for idx := range s.Identity.VerifiableAddresses { + va := s.Identity.VerifiableAddresses[idx] + if !va.Verified && recoveryCode.Address == va.Value { + va.Verified = true + va.Status = identity.VerifiableAddressStatusCompleted + if err := cv.r.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &va); err != nil { + return err + } + break + } + } + + return nil +} diff --git a/selfservice/hook/code_address_verifier_test.go b/selfservice/hook/code_address_verifier_test.go new file mode 100644 index 000000000000..b39f59dfbc2e --- /dev/null +++ b/selfservice/hook/code_address_verifier_test.go @@ -0,0 +1,100 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook_test + +import ( + "context" + "net/http" + "strings" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/randx" +) + +func TestCodeAddressVerifier(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.schema.json") + verifier := hook.NewCodeAddressVerifier(reg) + + setup := func(t *testing.T) (address string, rf *registration.Flow) { + t.Helper() + address = testhelpers.RandomEmail() + rawCode := strings.ToLower(randx.MustString(16, randx.Alpha)) + + rf = ®istration.Flow{Active: identity.CredentialsTypeCodeAuth, Type: "browser", State: flow.StatePassedChallenge} + require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(ctx, rf)) + + _, err := reg.RegistrationCodePersister().CreateRegistrationCode(ctx, &code.CreateRegistrationCodeParams{ + Address: address, + AddressType: identity.CodeAddressTypeEmail, + RawCode: rawCode, + ExpiresIn: time.Hour, + FlowID: rf.ID, + }) + require.NoError(t, err) + + _, err = reg.RegistrationCodePersister().UseRegistrationCode(ctx, rf.ID, rawCode, address) + require.NoError(t, err) + + return + } + + setupIdentity := func(t *testing.T, address string) *identity.Identity { + t.Helper() + verifiableAddress := []identity.VerifiableAddress{{ID: uuid.UUID{}, Verified: false, Value: address, Via: identity.VerifiableAddressTypeEmail}} + id := &identity.Identity{ID: x.NewUUID(), VerifiableAddresses: verifiableAddress, Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: {Type: identity.CredentialsTypeCodeAuth, Identifiers: []string{address}}, + }} + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, id)) + return id + } + + runHook := func(t *testing.T, id *identity.Identity, flow *registration.Flow) { + t.Helper() + + sessions := &session.Session{ + ID: x.NewUUID(), + Identity: id, + } + + r := &http.Request{} + require.NoError(t, verifier.ExecutePostRegistrationPostPersistHook(nil, r, flow, sessions)) + } + + t.Run("case=should set the verifiable email address to verified", func(t *testing.T) { + address, flow := setup(t) + id := setupIdentity(t, address) + + runHook(t, id, flow) + va, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, address) + require.NoError(t, err) + require.True(t, va.Verified) + }) + + t.Run("case=should ignore verifiable email address that does not match the code", func(t *testing.T) { + _, flow := setup(t) + newEmail := testhelpers.RandomEmail() + id := setupIdentity(t, newEmail) + + runHook(t, id, flow) + va, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, newEmail) + require.NoError(t, err) + require.False(t, va.Verified) + }) +} diff --git a/selfservice/hook/stub/code.schema.json b/selfservice/hook/stub/code.schema.json new file mode 100644 index 000000000000..71219c0b9db4 --- /dev/null +++ b/selfservice/hook/stub/code.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } + } + } +} diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index 8f8deea73eb9..ecf1f704cbaa 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -18,8 +18,10 @@ import ( "github.com/ory/x/otelx" ) -var _ registration.PostHookPostPersistExecutor = new(Verifier) -var _ settings.PostHookPostPersistExecutor = new(Verifier) +var ( + _ registration.PostHookPostPersistExecutor = new(Verifier) + _ settings.PostHookPostPersistExecutor = new(Verifier) +) type ( verifierDependencies interface { @@ -87,7 +89,7 @@ func (e *Verifier) do(w http.ResponseWriter, r *http.Request, i *identity.Identi verificationFlow.OAuth2LoginChallenge = f.GetOAuth2LoginChallenge() } - verificationFlow.State = verification.StateEmailSent + verificationFlow.State = flow.StateEmailSent if err := strategy.PopulateVerificationMethod(r, verificationFlow); err != nil { return err diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 1013de192223..3d4195b6e0ba 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -22,7 +22,6 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" - "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" "github.com/ory/kratos/x" @@ -94,7 +93,7 @@ func TestVerifier(t *testing.T) { expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) require.NoError(t, err) - require.Equal(t, expectedVerificationFlow.State, verification.StateEmailSent) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) require.NoError(t, err) @@ -110,7 +109,7 @@ func TestVerifier(t *testing.T) { // Email to baz@ory.sh is skipped because it is verified already. assert.NotContains(t, recipients, "baz@ory.sh") - //these addresses will be marked as sent and won't be sent again by the settings hook + // these addresses will be marked as sent and won't be sent again by the settings hook address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") require.NoError(t, err) assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json index 6572ba9f9d89..fa030cbd67a0 100644 --- a/selfservice/strategy/code/.schema/login.schema.json +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -12,9 +12,14 @@ "code": { "type": "string" }, - "email": { + "identifier": { + "type": "string" + }, + "resend": { "type": "string", - "format": "email" + "enum": [ + "code" + ] }, "flow": { "type": "string", diff --git a/selfservice/strategy/code/.schema/registration.schema.json b/selfservice/strategy/code/.schema/registration.schema.json new file mode 100644 index 000000000000..90f245c107c5 --- /dev/null +++ b/selfservice/strategy/code/.schema/registration.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/code/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "code" + ] + }, + "csrf_token": { + "type": "string" + }, + "code": { + "type": "string" + }, + "resend": { + "type": "string", + "enum": [ + "code" + ] + }, + "traits": { + "description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect." + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index a1993da4d3d0..42456da54dc5 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -1,4 +1,17 @@ [ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "hidden", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, { "type": "input", "group": "code", @@ -18,19 +31,6 @@ } } }, - { - "type": "input", - "group": "code", - "attributes": { - "name": "method", - "type": "hidden", - "value": "code", - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": {} - }, { "type": "input", "group": "code", diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go index 9243f1015e26..7c183413799d 100644 --- a/selfservice/strategy/code/code_login.go +++ b/selfservice/strategy/code/code_login.go @@ -1,22 +1,101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package code import ( + "context" "database/sql" + "time" "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" ) -type LoginRegistrationCode struct { - // ID is the primary key +// swagger:ignore +type LoginCode struct { + // ID represents the tokens's unique ID. // // required: true // type: string // format: uuid ID uuid.UUID `json:"id" db:"id" faker:"-"` - // CodeHMAC represents the HMACed value of the login/registration code - CodeHMAC string `json:"-" db:"code_hmac"` + // Address represents the address that the code was sent to. + // this can be an email address or a phone number. + Address string `json:"-" db:"address"` + + // AddressType represents the type of the address + // this can be an email address or a phone number. + AddressType identity.CodeAddressType `json:"-" db:"address_type"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code"` // UsedAt is the timestamp of when the code was used or null if it wasn't yet UsedAt sql.NullTime `json:"-" db:"used_at"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_login_flow_id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + IdentityID uuid.UUID `json:"identity_id" faker:"-" db:"identity_id"` +} + +func (LoginCode) TableName(ctx context.Context) string { + return "identity_login_codes" +} + +func (f LoginCode) IsExpired() bool { + return f.ExpiresAt.Before(time.Now()) +} + +func (r LoginCode) WasUsed() bool { + return r.UsedAt.Valid +} + +func (f LoginCode) IsValid() bool { + return !f.IsExpired() && !f.WasUsed() +} + +// swagger:ignore +type CreateLoginCodeParams struct { + // Address is the email address or phone number the code should be sent to. + // required: true + Address string + + // AddressType is the type of the address (email or phone number). + // required: true + AddressType identity.CodeAddressType + + // Code represents the recovery code + // required: true + RawCode string + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // FlowID is a helper struct field for gobuffalo.pop. + // required: true + FlowID uuid.UUID + + // IdentityID is the identity that this code is for + // required: true + IdentityID uuid.UUID } diff --git a/selfservice/strategy/code/code_registration.go b/selfservice/strategy/code/code_registration.go new file mode 100644 index 000000000000..256914760782 --- /dev/null +++ b/selfservice/strategy/code/code_registration.go @@ -0,0 +1,96 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" +) + +// swagger:ignore +type RegistrationCode struct { + // ID represents the tokens's unique ID. + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // Address represents the address that the code was sent to. + // this can be an email address or a phone number. + Address string `json:"-" db:"address"` + + // AddressType represents the type of the address + // this can be an email address or a phone number. + AddressType identity.CodeAddressType `json:"-" db:"address_type"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_registration_flow_id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` +} + +func (RegistrationCode) TableName(ctx context.Context) string { + return "identity_registration_codes" +} + +func (f RegistrationCode) IsExpired() bool { + return f.ExpiresAt.Before(time.Now()) +} + +func (r RegistrationCode) WasUsed() bool { + return r.UsedAt.Valid +} + +func (f RegistrationCode) IsValid() bool { + return !f.IsExpired() && !f.WasUsed() +} + +// swagger:ignore +type CreateRegistrationCodeParams struct { + // Address is the email address or phone number the code should be sent to. + // required: true + Address string + + // AddressType is the type of the address (email or phone number). + // required: true + AddressType identity.CodeAddressType + + // Code represents the recovery code + // required: true + RawCode string + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // FlowID is a helper struct field for gobuffalo.pop. + // required: true + FlowID uuid.UUID +} diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index 5f48437131b5..db3af496b59e 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -22,6 +22,7 @@ import ( "github.com/ory/kratos/courier" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/x" @@ -40,6 +41,8 @@ type ( RecoveryCodePersistenceProvider VerificationCodePersistenceProvider + RegistrationCodePersistenceProvider + LoginCodePersistenceProvider HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client } @@ -50,6 +53,10 @@ type ( Sender struct { deps senderDependencies } + Address struct { + To string + Via identity.CodeAddressType + } ) var ErrUnknownAddress = herodot.ErrNotFound.WithReason("recovery requested for unknown address") @@ -58,6 +65,93 @@ func NewSender(deps senderDependencies) *Sender { return &Sender{deps: deps} } +func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identity, addresses ...Address) error { + s.deps.Logger(). + WithSensitiveField("address", addresses). + Debugf("Preparing %s code", f.GetFlowName()) + + // send to all addresses + for _, address := range addresses { + rawCode := GenerateCode() + + switch f.GetFlowName() { + case flow.RegistrationFlow: + code, err := s.deps. + RegistrationCodePersister(). + CreateRegistrationCode(ctx, &CreateRegistrationCodeParams{ + AddressType: address.Via, + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + FlowID: f.GetID(), + Address: address.To, + }) + if err != nil { + return err + } + model, err := x.StructToMap(id.Traits) + if err != nil { + return err + } + + emailModel := email.RegistrationCodeValidModel{ + To: address.To, + RegistrationCode: rawCode, + Traits: model, + } + + s.deps.Audit(). + WithField("registration_flow_id", code.FlowID). + WithField("registration_code_id", code.ID). + WithSensitiveField("registration_code", rawCode). + Info("Sending out registration email with code.") + + if err := s.send(ctx, string(address.Via), email.NewRegistrationCodeValid(s.deps, &emailModel)); err != nil { + return errors.WithStack(err) + } + + case flow.LoginFlow: + code, err := s.deps. + LoginCodePersister(). + CreateLoginCode(ctx, &CreateLoginCodeParams{ + AddressType: address.Via, + Address: address.To, + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + FlowID: f.GetID(), + IdentityID: id.ID, + }) + if err != nil { + return err + } + + model, err := x.StructToMap(id) + if err != nil { + return err + } + + emailModel := email.LoginCodeValidModel{ + To: address.To, + LoginCode: rawCode, + Identity: model, + } + s.deps.Audit(). + WithField("login_flow_id", code.FlowID). + WithField("login_code_id", code.ID). + WithSensitiveField("login_code", rawCode). + Info("Sending out login email with code.") + + if err := s.send(ctx, string(address.Via), email.NewLoginCodeValid(s.deps, &emailModel)); err != nil { + return errors.WithStack(err) + } + + default: + return errors.WithStack(errors.New("received unknown flow type")) + + } + } + return nil +} + // SendRecoveryCode sends a recovery code to the specified address // // If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is diff --git a/selfservice/strategy/code/code_sender_test.go b/selfservice/strategy/code/code_sender_test.go index 6b74bf4f1c53..e5ba75826eb5 100644 --- a/selfservice/strategy/code/code_sender_test.go +++ b/selfservice/strategy/code/code_sender_test.go @@ -48,7 +48,6 @@ func TestSender(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(ctx, i)) t.Run("method=SendRecoveryCode", func(t *testing.T) { - recoveryCode := func(t *testing.T) { t.Helper() f, err := recovery.NewFlow(conf, time.Hour, "", u, code.NewStrategy(reg), flow.TypeBrowser) @@ -101,7 +100,6 @@ func TestSender(t *testing.T) { assert.Equal(t, messages[1].Subject, subject+" invalid") assert.Equal(t, messages[1].Body, body) }) - }) t.Run("method=SendVerificationCode", func(t *testing.T) { @@ -198,7 +196,6 @@ func TestSender(t *testing.T) { }, } { t.Run("strategy="+tc.flow, func(t *testing.T) { - conf.Set(ctx, tc.configKey, false) t.Cleanup(func() { @@ -214,5 +211,4 @@ func TestSender(t *testing.T) { }) } }) - } diff --git a/selfservice/strategy/code/persistence.go b/selfservice/strategy/code/persistence.go index b64a6bbfb0c8..ea5aaff682cc 100644 --- a/selfservice/strategy/code/persistence.go +++ b/selfservice/strategy/code/persistence.go @@ -29,4 +29,26 @@ type ( VerificationCodePersistenceProvider interface { VerificationCodePersister() VerificationCodePersister } + + RegistrationCodePersistenceProvider interface { + RegistrationCodePersister() RegistrationCodePersister + } + + RegistrationCodePersister interface { + CreateRegistrationCode(context.Context, *CreateRegistrationCodeParams) (*RegistrationCode, error) + UseRegistrationCode(ctx context.Context, flowID uuid.UUID, code string, addresses ...string) (*RegistrationCode, error) + DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*RegistrationCode, error) + } + + LoginCodePersistenceProvider interface { + LoginCodePersister() LoginCodePersister + } + + LoginCodePersister interface { + CreateLoginCode(context.Context, *CreateLoginCodeParams) (*LoginCode, error) + UseLoginCode(ctx context.Context, flowID uuid.UUID, identityID uuid.UUID, code string) (*LoginCode, error) + DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + GetUsedLoginCode(ctx context.Context, flowID uuid.UUID) (*LoginCode, error) + } ) diff --git a/selfservice/strategy/code/schema.go b/selfservice/strategy/code/schema.go index 24674c9a476d..d3ec2c66cf81 100644 --- a/selfservice/strategy/code/schema.go +++ b/selfservice/strategy/code/schema.go @@ -15,3 +15,6 @@ var verificationMethodSchema []byte //go:embed .schema/login.schema.json var loginMethodSchema []byte + +//go:embed .schema/registration.schema.json +var registrationSchema []byte diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index 262cc40e8963..3438181965ae 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -4,34 +4,53 @@ package code import ( + "context" + "encoding/json" + "net/http" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" "github.com/ory/kratos/courier" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/session" + "github.com/ory/kratos/text" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/decoderx" "github.com/ory/x/randx" + "github.com/ory/x/urlx" ) -var _ recovery.Strategy = new(Strategy) -var _ recovery.AdminHandler = new(Strategy) -var _ recovery.PublicHandler = new(Strategy) +var ( + _ recovery.Strategy = new(Strategy) + _ recovery.AdminHandler = new(Strategy) + _ recovery.PublicHandler = new(Strategy) +) -var _ verification.Strategy = new(Strategy) -var _ verification.AdminHandler = new(Strategy) -var _ verification.PublicHandler = new(Strategy) +var ( + _ verification.Strategy = new(Strategy) + _ verification.AdminHandler = new(Strategy) + _ verification.PublicHandler = new(Strategy) +) -var _ login.Strategy = new(Strategy) -var _ registration.Strategy = new(Strategy) +var ( + _ login.Strategy = new(Strategy) + _ registration.Strategy = new(Strategy) +) type ( // FlowMethod contains the configuration for this selfservice strategy. @@ -71,16 +90,23 @@ type ( verification.HookExecutorProvider login.StrategyProvider - login.HookExecutorProvider login.FlowPersistenceProvider registration.StrategyProvider + registration.FlowPersistenceProvider RecoveryCodePersistenceProvider VerificationCodePersistenceProvider SenderProvider + RegistrationCodePersistenceProvider + LoginCodePersistenceProvider + schema.IdentityTraitsProvider + + sessiontokenexchange.PersistenceProvider + + continuity.ManagementProvider } Strategy struct { @@ -93,10 +119,319 @@ func NewStrategy(deps strategyDependencies) *Strategy { return &Strategy{deps: deps, dx: decoderx.NewHTTP()} } +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeCodeAuth +} + func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.CodeGroup } +func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { + if string(f.GetState()) == "" { + f.SetState(flow.StateChooseMethod) + } + + f.GetUI().ResetMessages() + + nodes := f.GetUI().Nodes + + switch f.GetState() { + case flow.StateChooseMethod: + + if f.GetFlowName() == flow.VerificationFlow || f.GetFlowName() == flow.RecoveryFlow { + nodes.Append( + node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + } else if f.GetFlowName() == flow.LoginFlow { + // we use the identifier label here since we don't know what + // type of field the identifier is + nodes.Upsert( + node.NewInputField("identifier", nil, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeLabelID()), + ) + } else if f.GetFlowName() == flow.RegistrationFlow { + ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + // set the traits on the default group so that the ui can render them + // this prevents having multiple of the same ui fields on the same ui form + traitNodes, err := container.NodesFromJSONSchema(r.Context(), node.CodeGroup, ds.String(), "", nil) + if err != nil { + return err + } + + for _, n := range traitNodes { + nodes.Append(n) + } + } + + var codeMetaLabel *text.Message + + switch f.GetFlowName() { + case flow.VerificationFlow, flow.RecoveryFlow: + codeMetaLabel = text.NewInfoNodeLabelSubmit() + case flow.LoginFlow: + codeMetaLabel = text.NewInfoSelfServiceLoginCode() + case flow.RegistrationFlow: + codeMetaLabel = text.NewInfoSelfServiceRegistrationRegisterCode() + } + + methodButton := node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(codeMetaLabel) + + nodes.Append(methodButton) + + f.GetUI().Nodes = nodes + + case flow.StateEmailSent: + // fresh ui node group + freshNodes := node.Nodes{} + var route string + var codeMetaLabel *text.Message + var message *text.Message + + var resendNode *node.Node + + switch f.GetFlowName() { + case flow.RecoveryFlow: + route = recovery.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelRecoveryCode() + message = text.NewRecoveryEmailWithCodeSent() + + resendNode = node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeResendOTP()) + case flow.VerificationFlow: + route = verification.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelVerificationCode() + message = text.NewVerificationEmailWithCodeSent() + + case flow.LoginFlow: + route = login.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelLoginCode() + message = text.NewLoginEmailWithCodeSent() + + // preserve the login identifier that were submitted + // so we can retry the code flow with the same data + for _, n := range f.GetUI().Nodes { + if n.Group == node.DefaultGroup { + freshNodes = append(freshNodes, n) + } + } + + resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()) + + case flow.RegistrationFlow: + route = registration.RouteSubmitFlow + codeMetaLabel = text.NewInfoNodeLabelRegistrationCode() + message = text.NewRegistrationEmailWithCodeSent() + + // in the registration flow we need to preserve the trait fields that were submitted + // so we can retry the code flow with the same data + for _, n := range f.GetUI().Nodes { + if t, ok := n.Attributes.(*node.InputAttributes); ok && t.Type == node.InputAttributeTypeSubmit { + continue + } + + if n.Group == node.CodeGroup { + freshNodes = append(freshNodes, n) + } + } + + resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeResendOTP()) + default: + return errors.WithStack(herodot.ErrBadRequest.WithReason("received an unexpected flow type")) + } + + // Hidden field Required for the re-send code button + // !!important!!: this field must be appended before the code submit button since upsert will replace the first node with the same name + freshNodes.Upsert( + node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), + ) + + // code input field + freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + WithMetaLabel(codeMetaLabel)) + + // code submit button + freshNodes. + Append(node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelSubmit())) + + if resendNode != nil { + freshNodes.Append(resendNode) + } + + f.GetUI().Nodes = freshNodes + + f.GetUI().Method = "POST" + f.GetUI().Action = flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), route), f.GetID()).String() + + // Set the request's CSRF token + if f.GetType() == flow.TypeBrowser { + f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r)) + } + + f.GetUI().Messages.Set(message) + + case flow.StatePassedChallenge: + fallthrough + default: + return errors.WithStack(herodot.ErrBadRequest.WithReason("received an unexpected flow state")) + } + + // no matter the flow type or state we need to set the CSRF token + if f.GetType() == flow.TypeBrowser { + f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r)) + } + return nil +} + +// NewCodeUINodes creates a fresh UI for the code flow. +// this is used with the `recovery`, `verification`, `registration` and `login` flows. +func (s *Strategy) NewCodeUINodes(r *http.Request, f flow.Flow, data json.RawMessage) error { + if err := s.PopulateMethod(r, f); err != nil { + return err + } + + // on Registration flow we need to populate the form with the values from the initial form generation + if f.GetFlowName() == flow.RegistrationFlow { + for _, n := range container.NewFromJSON("", node.CodeGroup, data, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.GetUI().GetNodes().SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } else if f.GetFlowName() == flow.LoginFlow { + // on Login flow we need to populate the form with the values from the initial form generation + for _, n := range container.NewFromJSON("", node.DefaultGroup, data, "").Nodes { + f.GetUI().GetNodes().SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } + + return nil +} + +type ( + CreateCodeState func(context.Context, *CodeStateManagerPayload) error + ValidateCodeState func(context.Context, *CodeStateManagerPayload) error + AlreadyValidatedCodeState func(context.Context, *CodeStateManagerPayload) error + CodeStateManager struct { + f flow.Flow + payload *CodeStateManagerPayload + createCodeState CreateCodeState + verifyCodeState ValidateCodeState + alreadyValidatedCodeState AlreadyValidatedCodeState + } + CodeStateManagerPayload struct { + Identifier string + Email string + Traits json.RawMessage + TransientPayload json.RawMessage + Resend string + Code string + } +) + +func NewCodeStateManager(f flow.Flow, payload *CodeStateManagerPayload) *CodeStateManager { + return &CodeStateManager{ + f: f, + payload: payload, + } +} + +func (c *CodeStateManager) SetCreateCodeHandler(fn CreateCodeState) { + c.createCodeState = fn +} + +func (c *CodeStateManager) SetCodeVerifyHandler(fn ValidateCodeState) { + c.verifyCodeState = fn +} + +func (c *CodeStateManager) SetCodeDoneHandler(fn AlreadyValidatedCodeState) { + c.alreadyValidatedCodeState = fn +} + +func (c *CodeStateManager) validatePayload(ctx context.Context) error { + switch c.f.GetFlowName() { + case flow.LoginFlow: + if len(c.payload.Identifier) == 0 { + return errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) + } + case flow.RegistrationFlow: + if len(c.payload.Traits) == 0 { + return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) + } + case flow.RecoveryFlow, flow.VerificationFlow: + if len(c.payload.Email) == 0 { + return errors.WithStack(schema.NewRequiredError("#/email", "email")) + } + default: + return errors.New("received unexpected flow type") + } + return nil +} + +func (c *CodeStateManager) Run(ctx context.Context) error { + // By Default the flow should be in the 'choose method' state. + if c.f.GetState() == "" { + c.f.SetState(flow.StateChooseMethod) + } + + if strings.EqualFold(c.payload.Resend, "code") { + c.f.SetState(flow.StateChooseMethod) + } + + switch c.f.GetState() { + case flow.StateChooseMethod: + // we are in the first submission state of the flow + + if err := c.validatePayload(ctx); err != nil { + return err + } + + if err := c.createCodeState(ctx, c.payload); err != nil { + return err + } + + case flow.StateEmailSent: + // we are in the second submission state of the flow + // we need to check the code and update the identity + if len(c.payload.Code) == 0 { + return errors.WithStack(schema.NewRequiredError("#/code", "code")) + } + + if err := c.validatePayload(ctx); err != nil { + return err + } + + if err := c.verifyCodeState(ctx, c.payload); err != nil { + return err + } + case flow.StatePassedChallenge: + return c.alreadyValidatedCodeState(ctx, c.payload) + default: + return errors.WithStack(errors.New("Unknown flow state")) + } + return nil +} + +func (s *Strategy) NextFlowState(f flow.Flow) { + switch f.GetState() { + case flow.StateChooseMethod: + f.SetState(flow.StateEmailSent) + case flow.StateEmailSent: + f.SetState(flow.StatePassedChallenge) + case flow.StatePassedChallenge: + f.SetState(flow.StatePassedChallenge) + default: + f.SetState(flow.StateChooseMethod) + } +} + const CodeLength = 6 func GenerateCode() string { diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index 126fea60d8b2..9f3ad0f64e2e 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -1,14 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package code import ( - "bytes" "context" "encoding/json" "net/http" + "strings" "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/ory/herodot" "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/session" @@ -16,33 +22,49 @@ import ( "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/decoderx" - "github.com/ory/x/stringsx" ) var _ login.Strategy = new(Strategy) -type loginSubmitPayload struct { - Method string `json:"method"` - CSRFToken string `json:"csrf_token"` - Code string `json:"code"` - Identifier string `json:"identifier"` -} +// Update Login flow using the code method +// +// swagger:model updateLoginFlowWithCodeMethod +type updateLoginFlowWithCodeMethod struct { + // Method should be set to "code" when logging in using the code strategy. + // + // required: true + Method string `json:"method" form:"method"` -func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) { -} + // CSRFToken is the anti-CSRF token + // + // required: true + CSRFToken string `json:"csrf_token" form:"csrf_token"` -func (s *Strategy) ID() identity.CredentialsType { - return identity.CredentialsTypeCodeAuth + // Code is the 6 digits code sent to the user + // + // required: false + Code string `json:"code" form:"code"` + + // Identifier is the code identifier + // The identifier requires that the user has already completed the registration or settings with code flow. + // required: false + Identifier string `json:"identifier" form:"identifier"` + + // Resend is set when the user wants to resend the code + // required: false + Resend string `json:"resend" form:"resend"` } +func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) {} + func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { return session.AuthenticationMethod{ Method: identity.CredentialsTypeCodeAuth, - AAL: identity.AuthenticatorAssuranceLevel2, + AAL: identity.AuthenticatorAssuranceLevel1, } } -func (s *Strategy) HandleLoginError(w http.ResponseWriter, r *http.Request, flow *login.Flow, body *loginSubmitPayload, err error) error { +func (s *Strategy) HandleLoginError(w http.ResponseWriter, r *http.Request, flow *login.Flow, body *updateLoginFlowWithCodeMethod, err error) error { if flow != nil { email := "" if body != nil { @@ -51,8 +73,8 @@ func (s *Strategy) HandleLoginError(w http.ResponseWriter, r *http.Request, flow flow.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) flow.UI.GetNodes().Upsert( - node.NewInputField("identifier", email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). - WithMetaLabel(text.NewInfoNodeInputEmail()), + node.NewInputField("identifier", email, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeLabelID()), ) } @@ -60,34 +82,50 @@ func (s *Strategy) HandleLoginError(w http.ResponseWriter, r *http.Request, flow } func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, lf *login.Flow) error { - if lf.Type != flow.TypeBrowser { - return nil + return s.PopulateMethod(r, lf) +} + +func (s *Strategy) getIdentity(ctx context.Context, identifier string) (*identity.Identity, *identity.Credentials, error) { + i, _, err := s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identifier) + if err != nil { + return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) } - if requestedAAL == identity.AuthenticatorAssuranceLevel2 { - return nil + if err := s.deps.IdentityValidator().Validate(ctx, i); err != nil { + return nil, nil, errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) } - lf.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - lf.UI.GetNodes().Upsert( - node.NewInputField("identifier", "", node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). - WithMetaLabel(text.NewInfoNodeInputEmail()), - ) - return nil + cred, ok := i.GetCredentials(s.ID()) + if !ok { + return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } else if len(cred.Identifiers) == 0 { + return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } else if cred.IdentifierAddressType == "" { + return nil, nil, errors.WithStack(schema.NewRequiredError("#/code", "via")) + } + + return i, cred, nil } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, identityID uuid.UUID) (i *identity.Identity, err error) { - if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + s.deps.Audit(). + WithRequest(r). + WithField("identity_id", identityID). + WithField("login_flow_id", f.ID). + Info("Login with the code strategy started.") + + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.deps); err != nil { + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { return nil, err } - var p loginSubmitPayload + var p updateLoginFlowWithCodeMethod if err := s.dx.Decode(r, &p, decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPKeepRequestBody(true), decoderx.MustHTTPRawJSONSchemaCompiler(loginMethodSchema), decoderx.HTTPDecoderAllowedMethods("POST"), decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { @@ -98,21 +136,154 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.HandleLoginError(w, r, f, &p, err) } - i, c, err := s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), p.Identifier) + codeManager := NewCodeStateManager(f, &CodeStateManagerPayload{ + Identifier: p.Identifier, + Resend: p.Resend, + Code: p.Code, + }) - if err != nil { - return nil, s.HandleLoginError(w, r, f, &p, err) - } + codeManager.SetCreateCodeHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { + s.deps.Audit(). + WithSensitiveField("identifier", p.Identifier). + Info("Creating login code state.") + + // Step 1: Get the identity + i, cred, err := s.getIdentity(ctx, p.Identifier) + if err != nil { + return err + } + + // Step 2: Delete any previous login codes for this flow ID + if err := s.deps.LoginCodePersister().DeleteLoginCodesOfFlow(ctx, f.ID); err != nil { + return errors.WithStack(err) + } + + var identifier string + for _, id := range cred.Identifiers { + if strings.EqualFold(p.Identifier, id) { + identifier = id + } + } + + addresses := []Address{ + { + To: identifier, + Via: identity.CodeAddressType(cred.IdentifierAddressType), + }, + } + + // kratos only supports `email` identifiers at the moment with the code method + // this is validated in the identity validation step above + if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { + return errors.WithStack(err) + } + + // sets the flow state to code sent + s.NextFlowState(f) + + nodeData, err := json.Marshal(struct { + Identifier string `json:"identifier"` + }{ + Identifier: p.Identifier, + }) + if err != nil { + return errors.WithStack(err) + } + + if err := s.NewCodeUINodes(r, f, nodeData); err != nil { + return err + } + + f.Active = identity.CredentialsTypeCodeAuth + if err = s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return err + } - var o identity.CredentialsOTP - d := json.NewDecoder(bytes.NewBuffer(c.Config)) - if err := d.Decode(&o); err != nil { - return nil, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err) + if x.IsJSONRequest(r) { + s.deps.Writer().Write(w, r, f) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) + } + + // we return an error to the flow handler so that it does not continue execution of the hooks. + // we are not done with the login flow yet. The user needs to verify the code and then we need to persist the identity. + return errors.WithStack(flow.ErrCompletedByStrategy) + }) + + codeManager.SetCodeVerifyHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { + s.deps.Audit(). + WithSensitiveField("code", p.Code). + WithSensitiveField("identifier", p.Identifier). + Debug("Verifying login code") + + // Step 1: Get the identity + i, _, err = s.getIdentity(ctx, p.Identifier) + if err != nil { + return err + } + + loginCode, err := s.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) + if err != nil { + if errors.Is(err, ErrCodeNotFound) { + return schema.NewLoginCodeInvalid() + } + return errors.WithStack(err) + } + + i, err = s.deps.PrivilegedIdentityPool().GetIdentity(ctx, loginCode.IdentityID, identity.ExpandDefault) + if err != nil { + return errors.WithStack(err) + } + + // Step 2: The code was correct + f.Active = identity.CredentialsTypeCodeAuth + + // since nothing has errored yet, we can assume that the code is correct + // and we can update the login flow + s.NextFlowState(f) + + if err := s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + for idx := range i.VerifiableAddresses { + va := i.VerifiableAddresses[idx] + if !va.Verified && loginCode.Address == va.Value { + va.Verified = true + va.Status = identity.VerifiableAddressStatusCompleted + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &va); err != nil { + return err + } + break + } + } + + return nil + }) + + codeManager.SetCodeDoneHandler(func(ctx context.Context, codePayload *CodeStateManagerPayload) error { + s.deps.Audit(). + WithSensitiveField("identifier", codePayload.Identifier). + Debug("The login flow has already been completed, but is being re-requested.") + return s.HandleLoginError(w, r, f, &p, errors.WithStack(schema.NewNoLoginStrategyResponsible())) + }) + + if err := codeManager.Run(r.Context()); err != nil { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return nil, err + } + // the error is already handled by the registered code states + return i, s.HandleLoginError(w, r, f, &p, err) } - f.Active = identity.CredentialsTypeCodeAuth - if err = s.deps.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { - return nil, s.HandleLoginError(w, r, f, &p, err) + // a precaution in case the code manager did not set the identity + if i == nil { + s.deps.Audit(). + WithSensitiveField("identifier", p.Identifier). + WithRequest(r). + WithField("login_flow", f). + Error("The code manager did not set the identity.") + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("the login flow did not complete successfully")) } return i, nil diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go new file mode 100644 index 000000000000..9e8035e09969 --- /dev/null +++ b/selfservice/strategy/code/strategy_login_test.go @@ -0,0 +1,353 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" +) + +func TestLoginCodeStrategy(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.login_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) + + _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + createIdentity := func(t *testing.T, moreIdentifiers ...string) *identity.Identity { + t.Helper() + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + email := testhelpers.RandomEmail() + + ids := fmt.Sprintf(`"email":"%s"`, email) + for i, identifier := range moreIdentifiers { + ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier) + } + + i.Traits = identity.Traits(fmt.Sprintf(`{%s}`, ids)) + + credentials := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, + identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")}, + identity.CredentialsTypeCodeAuth: {Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"address_type\": \"email\", \"used_at\": \"2023-07-26T16:59:06+02:00\"}")}, + } + i.Credentials = credentials + + var va []identity.VerifiableAddress + for _, identifier := range moreIdentifiers { + va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted}) + } + + va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted}) + + i.VerifiableAddresses = va + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + return i + } + + type state struct { + flowID string + csrfToken string + identity *identity.Identity + client *http.Client + loginCode string + identityEmail string + } + + createLoginFlow := func(t *testing.T, moreIdentifiers ...string) *state { + t.Helper() + + identity := createIdentity(t, moreIdentifiers...) + client := testhelpers.NewClientWithCookies(t) + + // 1. Initiate flow + resp, err := client.Get(public.URL + login.RouteInitBrowserFlow) + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + flowID := gjson.GetBytes(body, "id").String() + require.NotEmpty(t, flowID) + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmpty(t, csrfToken) + + require.NoError(t, resp.Body.Close()) + + loginEmail := gjson.Get(identity.Traits.String(), "email").String() + require.NotEmpty(t, loginEmail) + + return &state{ + flowID: flowID, + csrfToken: csrfToken, + identity: identity, + identityEmail: loginEmail, + client: client, + } + } + + type onSubmitAssertion func(t *testing.T, s *state, res *http.Response) + + submitLoginID := func(t *testing.T, s *state, submitAssertion onSubmitAssertion) *state { + t.Helper() + + payload := strings.NewReader(url.Values{ + "csrf_token": {s.csrfToken}, + "method": {"code"}, + "identifier": {s.identityEmail}, + }.Encode()) + + req, err := http.NewRequestWithContext(ctx, "POST", public.URL+login.RouteSubmitFlow+"?flow="+s.flowID, payload) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err := s.client.Do(req) + require.NoError(t, err) + + if submitAssertion != nil { + submitAssertion(t, s, resp) + return s + } + + require.EqualValues(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmpty(t, csrfToken) + + s.csrfToken = csrfToken + + require.NoError(t, resp.Body.Close()) + + return s + } + + submitLoginCode := func(t *testing.T, s *state, submitAssertion onSubmitAssertion) *state { + t.Helper() + + req, err := http.NewRequestWithContext(ctx, "POST", public.URL+login.RouteSubmitFlow+"?flow="+s.flowID, strings.NewReader(url.Values{ + "csrf_token": {s.csrfToken}, + "method": {"code"}, + "code": {s.loginCode}, + "identifier": {s.identityEmail}, + }.Encode())) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err := s.client.Do(req) + require.NoError(t, err) + + if submitAssertion != nil { + submitAssertion(t, s, resp) + return s + } + + var cookie *http.Cookie + for _, c := range resp.Cookies() { + cookie = c + } + require.Equal(t, cookie.Name, "ory_kratos_session") + require.NotEmpty(t, cookie.Value) + + return s + } + + t.Run("case=should be able to log in with code", func(t *testing.T) { + // create login flow + s := createLoginFlow(t) + + // submit email + s = submitLoginID(t, s, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + s.loginCode = loginCode + + // 3. Submit OTP + submitLoginCode(t, s, nil) + }) + + t.Run("case=should not be able to change submitted id on code submit", func(t *testing.T) { + // create login flow + s := createLoginFlow(t) + + // submit email + s = submitLoginID(t, s, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + s.loginCode = loginCode + s.identityEmail = "not-" + s.identityEmail + + // 3. Submit OTP + s = submitLoginCode(t, s, func(t *testing.T, s *state, resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + }) + }) + + t.Run("case=should not be able to proceed to code entry when the account is unknown", func(t *testing.T) { + s := createLoginFlow(t) + + s.identityEmail = testhelpers.RandomEmail() + + // submit email + s = submitLoginID(t, s, func(t *testing.T, s *state, resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + }) + }) + + t.Run("case=should not be able to use valid code after 5 attempts", func(t *testing.T) { + s := createLoginFlow(t) + + // submit email + s = submitLoginID(t, s, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + for i := 0; i < 5; i++ { + + s.loginCode = "111111" + + // 3. Submit OTP + s = submitLoginCode(t, s, func(t *testing.T, s *state, resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "The login code is invalid or has already been used") + }) + } + + s.loginCode = loginCode + // 3. Submit OTP + s = submitLoginCode(t, s, func(t *testing.T, s *state, resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) + + t.Run("case=code should expire", func(t *testing.T) { + ctx := context.Background() + + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "10ns") + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1h") + }) + + s := createLoginFlow(t) + + // submit email + s = submitLoginID(t, s, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + s.loginCode = loginCode + + submitLoginCode(t, s, func(t *testing.T, s *state, resp *http.Response) { + require.EqualValues(t, http.StatusGone, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + require.Contains(t, gjson.GetBytes(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") + }) + }) + + t.Run("case=on login with un-verified address, should verify it", func(t *testing.T) { + s := createLoginFlow(t, testhelpers.RandomEmail()) + + loginEmail := gjson.Get(s.identity.Traits.String(), "email_1").String() + require.NotEmpty(t, loginEmail) + + s.identityEmail = loginEmail + + var va *identity.VerifiableAddress + + for _, v := range s.identity.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } + + require.NotNil(t, va) + require.False(t, va.Verified) + + // submit email + s = submitLoginID(t, s, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, loginEmail, "Login to your account") + require.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + require.NotEmpty(t, loginCode) + + s.loginCode = loginCode + + // Submit OTP + s = submitLoginCode(t, s, nil) + + id, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, s.identity.ID, identity.ExpandEverything) + require.NoError(t, err) + + va = nil + + for _, v := range id.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } + + require.NotNil(t, va) + require.True(t, va.Verified) + }) +} diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 8d4a5e45986a..7fd946c593ae 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -177,23 +177,23 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. return } - flow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) + recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) if err != nil { s.deps.Writer().WriteError(w, r, err) return } - flow.DangerousSkipCSRFCheck = true - flow.State = recovery.StateEmailSent - flow.UI.Nodes = node.Nodes{} - flow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + recoveryFlow.DangerousSkipCSRFCheck = true + recoveryFlow.State = flow.StateEmailSent + recoveryFlow.UI.Nodes = node.Nodes{} + recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) - flow.UI.Nodes. + recoveryFlow.UI.Nodes. Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeLabelSubmit())) - if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, flow); err != nil { + if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil { s.deps.Writer().WriteError(w, r, err) return } @@ -213,7 +213,7 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. RawCode: rawCode, CodeType: RecoveryCodeTypeAdmin, ExpiresIn: expiresIn, - FlowID: flow.ID, + FlowID: recoveryFlow.ID, IdentityID: id.ID, }); err != nil { s.deps.Writer().WriteError(w, r, err) @@ -226,11 +226,11 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. Info("A recovery code has been created.") body := &recoveryCodeForIdentity{ - ExpiresAt: flow.ExpiresAt.UTC(), + ExpiresAt: recoveryFlow.ExpiresAt.UTC(), RecoveryLink: urlx.CopyWithQuery( s.deps.Config().SelfServiceFlowRecoveryUI(ctx), url.Values{ - "flow": {flow.ID.String()}, + "flow": {recoveryFlow.ID.String()}, }).String(), RecoveryCode: rawCode, } @@ -310,8 +310,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F f.UI.ResetMessages() // If the email is present in the submission body, the user needs a new code via resend - if f.State != recovery.StateChooseMethod && len(body.Email) == 0 { - if err := flow.MethodEnabledAndAllowed(ctx, sID, sID, s.deps); err != nil { + if f.State != flow.StateChooseMethod && len(body.Email) == 0 { + if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, sID, s.deps); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } return s.recoveryUseCode(w, r, body, f) @@ -327,29 +327,29 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return errors.WithStack(flow.ErrCompletedByStrategy) } - if err := flow.MethodEnabledAndAllowed(ctx, sID, body.Method, s.deps); err != nil { + if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, body.Method, s.deps); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } - flow, err := s.deps.RecoveryFlowPersister().GetRecoveryFlow(ctx, x.ParseUUID(body.Flow)) + recoveryFlow, err := s.deps.RecoveryFlowPersister().GetRecoveryFlow(ctx, x.ParseUUID(body.Flow)) if err != nil { - return s.HandleRecoveryError(w, r, flow, body, err) + return s.HandleRecoveryError(w, r, recoveryFlow, body, err) } - if err := flow.Valid(); err != nil { - return s.HandleRecoveryError(w, r, flow, body, err) + if err := recoveryFlow.Valid(); err != nil { + return s.HandleRecoveryError(w, r, recoveryFlow, body, err) } - switch flow.State { - case recovery.StateChooseMethod: + switch recoveryFlow.State { + case flow.StateChooseMethod: fallthrough - case recovery.StateEmailSent: - return s.recoveryHandleFormSubmission(w, r, flow, body) - case recovery.StatePassedChallenge: + case flow.StateEmailSent: + return s.recoveryHandleFormSubmission(w, r, recoveryFlow, body) + case flow.StatePassedChallenge: // was already handled, do not allow retry - return s.retryRecoveryFlowWithMessage(w, r, flow.Type, text.NewErrorValidationRecoveryRetrySuccess()) + return s.retryRecoveryFlowWithMessage(w, r, recoveryFlow.Type, text.NewErrorValidationRecoveryRetrySuccess()) default: - return s.retryRecoveryFlowWithMessage(w, r, flow.Type, text.NewErrorValidationRecoveryStateFailure()) + return s.retryRecoveryFlowWithMessage(w, r, recoveryFlow.Type, text.NewErrorValidationRecoveryStateFailure()) } } @@ -357,7 +357,7 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, ctx := r.Context() f.UI.Messages.Clear() - f.State = recovery.StatePassedChallenge + f.State = flow.StatePassedChallenge f.SetCSRFToken(s.deps.CSRFHandler().RegenerateToken(w, r)) f.RecoveredIdentityID = uuid.NullUUID{ UUID: id.ID, @@ -540,7 +540,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) f.Active = sqlxx.NullString(s.NodeGroup()) - f.State = recovery.StateEmailSent + f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewRecoveryEmailWithCodeSent()) f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go index e07c0fb28f50..11fdf8431b9e 100644 --- a/selfservice/strategy/code/strategy_recovery_test.go +++ b/selfservice/strategy/code/strategy_recovery_test.go @@ -412,7 +412,7 @@ func TestRecovery(t *testing.T) { assert.Len(t, gjson.Get(recoverySubmissionResponse, "ui.messages").Array(), 1, "%s", recoverySubmissionResponse) assertx.EqualAsJSON(t, text.NewRecoveryEmailWithCodeSent(), json.RawMessage(gjson.Get(recoverySubmissionResponse, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by entering the following code") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -697,7 +697,7 @@ func TestRecovery(t *testing.T) { assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==code).attributes.value").String(), "%s", body) assertx.EqualAsJSON(t, text.NewRecoveryEmailWithCodeSent(), json.RawMessage(gjson.Get(body, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Account access attempted") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Account access attempted") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } @@ -734,7 +734,7 @@ func TestRecovery(t *testing.T) { addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, email) assert.NoError(t, err) - emailText := testhelpers.CourierExpectMessage(t, reg, email, "Recover access to your account") + emailText := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, emailText, 1) // Deactivate the identity @@ -773,7 +773,7 @@ func TestRecovery(t *testing.T) { actual := expectSuccessfulRecovery(t, cl, RecoveryFlowTypeBrowser, func(v url.Values) { v.Set("email", email) }) - message := testhelpers.CourierExpectMessage(t, reg, email, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -834,7 +834,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }, http.StatusOK) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) form := withCSRFToken(t, testCase.FlowType, actual, url.Values{ @@ -945,7 +945,7 @@ func TestRecovery(t *testing.T) { initialFlowId := gjson.Get(body, "id") - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by entering the following code") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -1000,7 +1000,7 @@ func TestRecovery(t *testing.T) { assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, recoveryEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) submitRecoveryCode(t, c, body, RecoveryFlowTypeBrowser, recoveryCode, http.StatusOK) @@ -1019,14 +1019,14 @@ func TestRecovery(t *testing.T) { require.NotEmpty(t, action) assert.Equal(t, recoveryEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message1 := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message1 := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode1 := testhelpers.CourierExpectCodeInMessage(t, message1, 1) body = resendRecoveryCode(t, c, body, RecoveryFlowTypeBrowser, http.StatusOK) assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, recoveryEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message2 := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message2 := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode2 := testhelpers.CourierExpectCodeInMessage(t, message2, 1) body = submitRecoveryCode(t, c, body, RecoveryFlowTypeBrowser, recoveryCode1, http.StatusOK) diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go new file mode 100644 index 000000000000..96cc224a8c3c --- /dev/null +++ b/selfservice/strategy/code/strategy_registration.go @@ -0,0 +1,267 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "context" + "database/sql" + "encoding/json" + "net/http" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +var _ registration.Strategy = new(Strategy) + +// Update Registration Flow with Code Method +// +// swagger:model updateRegistrationFlowWithCodeMethod +type updateRegistrationFlowWithCodeMethod struct { + // The identity's traits + // + // required: true + Traits json.RawMessage `json:"traits" form:"traits"` + + // The OTP Code sent to the user + // + // required: false + Code string `json:"code" form:"code"` + + // The CSRF Token + CSRFToken string `json:"csrf_token" form:"csrf_token"` + + // Method to use + // + // This field must be set to `code` when using the code method. + // + // required: true + Method string `json:"method" form:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` + + // Resend restarts the flow with a new code + // + // required: false + Resend string `json:"resend" form:"resend"` +} + +func (s *Strategy) RegisterRegistrationRoutes(*x.RouterPublic) {} + +func (s *Strategy) HandleRegistrationError(w http.ResponseWriter, r *http.Request, flow *registration.Flow, body *updateRegistrationFlowWithCodeMethod, err error) error { + if flow != nil { + if body != nil { + action := flow.AppendTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), registration.RouteSubmitFlow)).String() + for _, n := range container.NewFromJSON(action, node.CodeGroup, body.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + flow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } + + flow.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + } + + return err +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, rf *registration.Flow) error { + return s.PopulateMethod(r, rf) +} + +type options func(*identity.Identity) error + +func WithCredentials(via identity.CodeAddressType, usedAt sql.NullTime) options { + return func(i *identity.Identity) error { + return i.SetCredentialsWithConfig(identity.CredentialsTypeCodeAuth, identity.Credentials{Type: identity.CredentialsTypePassword, Identifiers: []string{}}, &identity.CredentialsCode{AddressType: via, UsedAt: usedAt}) + } +} + +func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flow, traits json.RawMessage, transientPayload json.RawMessage, i *identity.Identity, opts ...options) error { + f.TransientPayload = transientPayload + if len(traits) == 0 { + traits = json.RawMessage("{}") + } + + // we explicitly set the Code credentials type + i.Traits = identity.Traits(traits) + if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, &identity.CredentialsCode{UsedAt: sql.NullTime{}}); err != nil { + return err + } + + for _, opt := range opts { + if err := opt(i); err != nil { + return err + } + } + + // Validate the identity + if err := s.deps.IdentityValidator().Validate(ctx, i); err != nil { + return err + } + + return nil +} + +func (s *Strategy) getCredentialsFromTraits(ctx context.Context, f *registration.Flow, i *identity.Identity, traits, transientPayload json.RawMessage) (*identity.Credentials, error) { + if err := s.handleIdentityTraits(ctx, f, traits, transientPayload, i); err != nil { + return nil, errors.WithStack(err) + } + + cred, ok := i.GetCredentials(identity.CredentialsTypeCodeAuth) + if !ok { + return nil, errors.WithStack(schema.NewMissingIdentifierError()) + } else if len(cred.Identifiers) == 0 { + return nil, errors.WithStack(schema.NewMissingIdentifierError()) + } + return cred, nil +} + +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) error { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { + return err + } + + var p updateRegistrationFlowWithCodeMethod + if err := registration.DecodeBody(&p, r, s.dx, s.deps.Config(), registrationSchema); err != nil { + return s.HandleRegistrationError(w, r, f, &p, err) + } + + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(r.Context()), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { + return s.HandleRegistrationError(w, r, f, &p, err) + } + + codeManager := NewCodeStateManager(f, &CodeStateManagerPayload{ + Code: p.Code, + Traits: p.Traits, + Resend: p.Resend, + TransientPayload: p.TransientPayload, + }) + + codeManager.SetCreateCodeHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { + s.deps.Logger(). + WithSensitiveField("traits", p.Traits). + WithSensitiveField("transient_paylaod", p.TransientPayload). + Debug("Creating registration code.") + + // Create the Registration code + + // Step 1: validate the identity's traits + cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + if err != nil { + return err + } + + // Step 2: Delete any previous registration codes for this flow ID + if err := s.deps.RegistrationCodePersister().DeleteRegistrationCodesOfFlow(ctx, f.ID); err != nil { + return errors.WithStack(err) + } + + // Step 3: Get the identity email and send the code + var addresses []Address + for _, identifier := range cred.Identifiers { + addresses = append(addresses, Address{To: identifier, Via: identity.CodeAddressType(cred.IdentifierAddressType)}) + } + // kratos only supports `email` identifiers at the moment with the code method + // this is validated in the identity validation step above + if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { + return errors.WithStack(err) + } + + // sets the flow state to code sent + s.NextFlowState(f) + + // Step 4: Generate the UI for the `code` input form + // re-initialize the UI with a "clean" new state + // this should also provide a "resend" button and an option to change the email address + if err := s.NewCodeUINodes(r, f, p.Traits); err != nil { + return errors.WithStack(err) + } + + f.Active = identity.CredentialsTypeCodeAuth + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + if x.IsJSONRequest(r) { + s.deps.Writer().Write(w, r, f) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther) + } + + // we return an error to the flow handler so that it does not continue execution of the hooks. + // we are not done with the registration flow yet. The user needs to verify the code and then we need to persist the identity. + return errors.WithStack(flow.ErrCompletedByStrategy) + }) + + codeManager.SetCodeVerifyHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { + s.deps.Logger(). + WithSensitiveField("traits", p.Traits). + WithSensitiveField("transient_payload", p.TransientPayload). + WithSensitiveField("code", p.Code). + Debug("Verifying registration code") + + // Step 1: Re-validate the identity's traits + // this is important since the client could have switched out the identity's traits + // this method also returns the credentials for a temporary identity + cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + if err != nil { + return err + } + + // Step 2: Check if the flow traits match the identity traits + for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes { + if !strings.EqualFold(f.GetUI().GetNodes().Find(n.ID()).Attributes.GetValue().(string), n.Attributes.GetValue().(string)) { + return errors.WithStack(schema.NewTraitsMismatch()) + } + } + + // Step 3: Attempt to use the code + registrationCode, err := s.deps.RegistrationCodePersister().UseRegistrationCode(ctx, f.ID, p.Code, cred.Identifiers...) + if err != nil { + if errors.Is(err, ErrCodeNotFound) { + return errors.WithStack(schema.NewRegistrationCodeInvalid()) + } + return errors.WithStack(err) + } + + // Step 4: The code was correct, populate the Identity credentials and traits + if err := s.handleIdentityTraits(ctx, f, p.Traits, p.TransientPayload, i, WithCredentials(registrationCode.AddressType, registrationCode.UsedAt)); err != nil { + return errors.WithStack(err) + } + + // since nothing has errored yet, we can assume that the code is correct + // and we can update the registration flow + s.NextFlowState(f) + + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + + return nil + }) + + codeManager.SetCodeDoneHandler(func(ctx context.Context, _ *CodeStateManagerPayload) error { + return errors.WithStack(schema.NewNoRegistrationStrategyResponsible()) + }) + + if err := codeManager.Run(r.Context()); err != nil { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } + return s.HandleRegistrationError(w, r, f, &p, err) + } + return nil +} diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go new file mode 100644 index 000000000000..d070464a8cf7 --- /dev/null +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -0,0 +1,445 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + _ "embed" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/ioutilx" +) + +type state struct { + flowID string + csrfToken string + client *http.Client + email string +} + +func TestRegistrationCodeStrategyDisabled(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypePassword.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.registration_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), false) + + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + client := testhelpers.NewClientWithCookies(t) + resp, err := client.Get(public.URL + registration.RouteInitBrowserFlow) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Falsef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.value==code)").Exists(), "%s", body) + + // attempt to still submit the code form even though it doesn't exist + + payload := strings.NewReader(url.Values{ + "csrf_token": {gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String()}, + "method": {"code"}, + "traits.email": {testhelpers.RandomEmail()}, + }.Encode()) + req, err := http.NewRequestWithContext(ctx, "POST", public.URL+registration.RouteSubmitFlow+"?flow="+gjson.GetBytes(body, "id").String(), payload) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err = client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "This endpoint was disabled by system administrator. Please check your url or contact the system administrator to enable it.", gjson.GetBytes(body, "error.reason").String()) +} + +func TestRegistrationCodeStrategy(t *testing.T) { + setup := func(ctx context.Context, t *testing.T) (*config.Config, *driver.RegistryDefault, *httptest.Server) { + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypePassword.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.registration_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), true) + conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) + + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewErrorTestServer(t, reg) + + public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + return conf, reg, public + } + + createRegistrationFlow := func(ctx context.Context, t *testing.T, publicURL string) *state { + t.Helper() + + client := testhelpers.NewClientWithCookies(t) + req, err := http.NewRequestWithContext(ctx, "GET", publicURL+registration.RouteInitBrowserFlow, nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + flowID := gjson.GetBytes(body, "id").String() + require.NotEmpty(t, flowID) + + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmpty(t, csrfToken) + + require.Truef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email)").Exists(), "%s", body) + require.Truef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.value==code)").Exists(), "%s", body) + + require.NoError(t, resp.Body.Close()) + return &state{ + csrfToken: csrfToken, + client: client, + flowID: flowID, + } + } + + type onSubmitAssertion func(ctx context.Context, t *testing.T, s *state, resp *http.Response) + + registerNewUser := func(ctx context.Context, t *testing.T, publicURL string, s *state, submitAssertion onSubmitAssertion) *state { + t.Helper() + + email := testhelpers.RandomEmail() + + s.email = email + + payload := strings.NewReader(url.Values{ + "csrf_token": {s.csrfToken}, + "method": {"code"}, + "traits.email": {email}, + }.Encode()) + + req, err := http.NewRequestWithContext(ctx, "POST", publicURL+registration.RouteSubmitFlow+"?flow="+s.flowID, payload) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + client := s.client + + // 2. Submit Identifier (email) + resp, err := client.Do(req) + require.NoError(t, err) + if submitAssertion != nil { + submitAssertion(ctx, t, s, resp) + } else { + assert.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + assert.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, email, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + } + + require.NoError(t, resp.Body.Close()) + + return s + } + + submitOTP := func(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, publicURL string, s *state, otp string, shouldHaveSessionCookie bool, submitAssertion onSubmitAssertion) *state { + t.Helper() + + req, err := http.NewRequestWithContext(ctx, "POST", publicURL+registration.RouteSubmitFlow+"?flow="+s.flowID, strings.NewReader(url.Values{ + "csrf_token": {s.csrfToken}, + "method": {"code"}, + "code": {otp}, + "traits.email": {s.email}, + }.Encode())) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + // 3. Submit OTP + resp, err := s.client.Do(req) + require.NoError(t, err) + + if submitAssertion != nil { + submitAssertion(ctx, t, s, resp) + return s + } + + verifiableAddress, err := reg.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, s.email) + require.NoError(t, err) + require.Equal(t, s.email, verifiableAddress.Value) + + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, verifiableAddress.IdentityID) + require.NoError(t, err) + require.NotNil(t, id.ID) + + _, ok := id.GetCredentials(identity.CredentialsTypeCodeAuth) + require.True(t, ok) + + if shouldHaveSessionCookie { + // we should now end up with a session cookie + var sessionCookie *http.Cookie + for _, c := range resp.Cookies() { + if c.Name == "ory_kratos_session" { + sessionCookie = c + break + } + } + require.NotNil(t, sessionCookie) + require.NotEmpty(t, sessionCookie.Value) + } + return s + } + + t.Run("test=different flows on the same configurations", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + _, reg, public := setup(ctx, t) + + t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public.URL) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, public.URL, state, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, public.URL, state, registrationCode, true, nil) + }) + + t.Run("case=should be able to resend the code", func(t *testing.T) { + ctx := context.Background() + + s := createRegistrationFlow(ctx, t, public.URL) + + s = registerNewUser(ctx, t, public.URL, s, func(ctx context.Context, t *testing.T, s *state, resp *http.Response) { + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + csrfToken := gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, s.email, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + + attr := gjson.GetBytes(body, "ui.nodes.#(attributes.name==method)#").String() + require.NotEmpty(t, attr) + + val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() + require.Equal(t, "code", val) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // resend code + req, err := http.NewRequestWithContext(ctx, "POST", public.URL+registration.RouteSubmitFlow+"?flow="+s.flowID, strings.NewReader(url.Values{ + "csrf_token": {s.csrfToken}, + "method": {"code"}, + "resend": {"code"}, + "traits.email": {s.email}, + }.Encode())) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err := s.client.Do(req) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, resp.StatusCode) + + // get the new code from email + message = testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode2) + + require.NotEqual(t, registrationCode, registrationCode2) + + // try submit old code + s = submitOTP(ctx, t, reg, public.URL, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, gjson.GetBytes(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") + }) + + s = submitOTP(ctx, t, reg, public.URL, s, registrationCode2, true, nil) + }) + + t.Run("case=swapping out traits should not be possible on code submit", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public.URL) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, public.URL, s, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s.email = "not-" + s.email // swap out email + + // 3. Submit OTP + s = submitOTP(ctx, t, reg, public.URL, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + require.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + }) + }) + + t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public.URL) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, public.URL, s, nil) + + reg.Persister().Transaction(ctx, func(ctx context.Context, connection *pop.Connection) error { + count, err := connection.RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ?", new(code.RegistrationCode).TableName(ctx)), uuid.FromStringOrNil(s.flowID)).Count(new(code.RegistrationCode)) + require.NoError(t, err) + require.Equal(t, 1, count) + return nil + }) + + for i := 0; i < 5; i++ { + s = submitOTP(ctx, t, reg, public.URL, s, "111111", false, func(ctx context.Context, t *testing.T, s *state, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + require.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + }) + } + + s = submitOTP(ctx, t, reg, public.URL, s, "111111", false, func(ctx context.Context, t *testing.T, s *state, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + require.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) + }) + + t.Run("test=cases with different configs", func(t *testing.T) { + ctx := context.Background() + conf, reg, public := setup(ctx, t) + + t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public.URL) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, public.URL, s, func(ctx context.Context, t *testing.T, s *state, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, gjson.GetBytes(body, "ui.messages").String(), "Could not find any login identifiers") + }) + }) + + t.Run("case=should have verifiable address even if after session hook is disabled", func(t *testing.T) { + // disable the after session hook + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{}) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) + }) + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public.URL) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, public.URL, state, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, public.URL, state, registrationCode, false, nil) + }) + + t.Run("case=code should expire", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "10ns") + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1h") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public.URL) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, public.URL, s, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s = submitOTP(ctx, t, reg, public.URL, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, resp *http.Response) { + require.Equal(t, http.StatusGone, resp.StatusCode) + body := ioutilx.MustReadAll(resp.Body) + require.Contains(t, gjson.GetBytes(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") + }) + }) + }) +} diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index 4de4bf9d16a5..aea07b9f6ae9 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -38,35 +38,7 @@ func (s *Strategy) RegisterAdminVerificationRoutes(admin *x.RouterAdmin) { // Otherwise, the default email input is added. // If the flow is a browser flow, the CSRF token is added to the UI. func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error { - nodes := node.Nodes{} - switch f.State { - case verification.StateEmailSent: - nodes.Upsert( - node. - NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). - WithMetaLabel(text.NewInfoNodeLabelVerificationCode()), - ) - // Required for the re-send code button - nodes.Append( - node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), - ) - f.UI.Messages.Set(text.NewVerificationEmailWithCodeSent()) - default: - nodes.Upsert( - node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). - WithMetaLabel(text.NewInfoNodeInputEmail()), - ) - } - nodes.Append( - node.NewInputField("method", s.VerificationStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit()), - ) - - f.UI.Nodes = nodes - if f.Type == flow.TypeBrowser { - f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - } - return nil + return s.PopulateMethod(r, f) } func (s *Strategy) decodeVerification(r *http.Request) (*updateVerificationFlowWithCodeMethod, error) { @@ -156,7 +128,7 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio return s.handleVerificationError(w, r, nil, body, err) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil { return s.handleVerificationError(w, r, f, body, err) } @@ -165,11 +137,11 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } switch f.State { - case verification.StateChooseMethod: + case flow.StateChooseMethod: fallthrough - case verification.StateEmailSent: + case flow.StateEmailSent: return s.verificationHandleFormSubmission(w, r, f, body) - case verification.StatePassedChallenge: + case flow.StatePassedChallenge: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) default: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) @@ -177,7 +149,6 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } func (s *Strategy) handleLinkClick(w http.ResponseWriter, r *http.Request, f *verification.Flow, code string) error { - // Pre-fill the code if codeField := f.UI.Nodes.Find("code"); codeField != nil { codeField.Attributes.SetValue(code) @@ -230,7 +201,7 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht // Continue execution } - f.State = verification.StateEmailSent + f.State = flow.StateEmailSent if err := s.PopulateVerificationMethod(r, f); err != nil { return s.handleVerificationError(w, r, f, body, err) @@ -294,7 +265,7 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c Action: returnTo.String(), } - f.State = verification.StatePassedChallenge + f.State = flow.StatePassedChallenge // See https://github.com/ory/kratos/issues/1547 f.SetCSRFToken(flow.GetCSRFToken(s.deps, w, r, f.Type)) f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) @@ -378,7 +349,6 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http } func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress) (err error) { - rawCode := GenerateCode() code, err := s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{ @@ -387,7 +357,6 @@ func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Fl VerifiableAddress: a, FlowID: f.ID, }) - if err != nil { return err } diff --git a/selfservice/strategy/code/strategy_verification_test.go b/selfservice/strategy/code/strategy_verification_test.go index b00bcda5c2bf..9be8cd08145d 100644 --- a/selfservice/strategy/code/strategy_verification_test.go +++ b/selfservice/strategy/code/strategy_verification_test.go @@ -43,7 +43,7 @@ func TestVerification(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) initViper(t, ctx, conf) - var identityToVerify = &identity.Identity{ + identityToVerify := &identity.Identity{ ID: x.NewUUID(), Traits: identity.Traits(`{"email":"verifyme@ory.sh"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, @@ -56,7 +56,7 @@ func TestVerification(t *testing.T) { }, } - var verificationEmail = gjson.GetBytes(identityToVerify.Traits, "email").String() + verificationEmail := gjson.GetBytes(identityToVerify.Traits, "email").String() _ = testhelpers.NewVerificationUIFlowEchoServer(t, reg) _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) @@ -69,7 +69,7 @@ func TestVerification(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToVerify, identity.ManagerAllowWriteProtectedTraits)) - var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { if hc == nil { hc = testhelpers.NewDebugClient(t) if !isAPI { @@ -82,15 +82,15 @@ func TestVerification(t *testing.T) { testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) } - var expectValidationError = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) } - var expectSuccess = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, http.StatusOK) } - var submitVerificationCode = func(t *testing.T, body string, c *http.Client, code string) (string, *http.Response) { + submitVerificationCode := func(t *testing.T, body string, c *http.Client, code string) (string, *http.Response) { action := gjson.Get(body, "ui.action").String() require.NotEmpty(t, action, "%v", string(body)) csrfToken := extractCsrfToken([]byte(body)) @@ -135,14 +135,14 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require an email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, "Property email is missing.", gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Del("email") } @@ -160,7 +160,7 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require a valid email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string, value string) { + check := func(t *testing.T, actual string, value string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value), gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), @@ -168,7 +168,7 @@ func TestVerification(t *testing.T) { } for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -194,16 +194,16 @@ func TestVerification(t *testing.T) { }) var email string - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Someone tried to verify this email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Someone tried to verify this email address") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -282,7 +282,7 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by entering the following code") code := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -295,12 +295,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailWithCodeSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by entering the following code") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -335,7 +335,7 @@ func TestVerification(t *testing.T) { assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now())) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } @@ -353,13 +353,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address when the link is opened in another browser", func(t *testing.T) { - - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } expectSuccess(t, nil, false, false, values) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) code := testhelpers.CourierExpectCodeInMessage(t, message, 1) @@ -377,7 +376,7 @@ func TestVerification(t *testing.T) { newValidFlow := func(t *testing.T, fType flow.Type, requestURL string) (*verification.Flow, *code.VerificationCode, string) { f, err := verification.NewFlow(conf, time.Hour, x.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), code.NewStrategy(reg), fType) require.NoError(t, err) - f.State = verification.StateEmailSent + f.State = flow.StateEmailSent require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID) identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email) @@ -422,7 +421,7 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by entering the following code") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -459,7 +458,7 @@ func TestVerification(t *testing.T) { assert.Equal(t, text.ErrIDSelfServiceFlowReplaced, gjson.GetBytes(f2, "error.id").String()) }) - var resendVerificationCode = func(t *testing.T, client *http.Client, flow string, flowType string, statusCode int) string { + resendVerificationCode := func(t *testing.T, client *http.Client, flow string, flowType string, statusCode int) string { action := gjson.Get(flow, "ui.action").String() assert.NotEmpty(t, action) @@ -487,7 +486,7 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") _ = testhelpers.CourierExpectCodeInMessage(t, message, 1) c := testhelpers.NewClientWithCookies(t) @@ -496,19 +495,18 @@ func TestVerification(t *testing.T) { assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, verificationEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message = testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") verificationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) submitVerificationCode(t, body, c, verificationCode) }) t.Run("case=should not be able to use first code after resending code", func(t *testing.T) { - body := expectSuccess(t, nil, true, false, func(v url.Values) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") firstCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) c := testhelpers.NewClientWithCookies(t) @@ -517,7 +515,7 @@ func TestVerification(t *testing.T) { assert.True(t, gjson.Get(body, "ui.nodes.#(attributes.name==code)").Exists()) assert.Equal(t, verificationEmail, gjson.Get(body, "ui.nodes.#(attributes.name==email).attributes.value").String()) - message = testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") secondCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) body, res := submitVerificationCode(t, body, c, firstCode) @@ -568,7 +566,7 @@ func TestVerification(t *testing.T) { body := expectSuccess(t, nil, true, false, func(v url.Values) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") code := testhelpers.CourierExpectCodeInMessage(t, message, 1) body, res := submitVerificationCode(t, body, c, code) @@ -578,7 +576,7 @@ func TestVerification(t *testing.T) { body = expectSuccess(t, nil, true, false, func(v url.Values) { v.Set("email", verificationEmail) }) - message = testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message = testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") code = testhelpers.CourierExpectCodeInMessage(t, message, 1) body, res = submitVerificationCode(t, body, c, code) @@ -636,5 +634,4 @@ func TestVerification(t *testing.T) { }) } }) - } diff --git a/selfservice/strategy/code/stub/code.identity.schema.json b/selfservice/strategy/code/stub/code.identity.schema.json new file mode 100644 index 000000000000..f8d988c21af9 --- /dev/null +++ b/selfservice/strategy/code/stub/code.identity.schema.json @@ -0,0 +1,61 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "email_0": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "email_1": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/strategy/link/strategy.go b/selfservice/strategy/link/strategy.go index fa5e9218a1df..da66e1816bf5 100644 --- a/selfservice/strategy/link/strategy.go +++ b/selfservice/strategy/link/strategy.go @@ -19,13 +19,17 @@ import ( "github.com/ory/x/decoderx" ) -var _ recovery.Strategy = new(Strategy) -var _ recovery.AdminHandler = new(Strategy) -var _ recovery.PublicHandler = new(Strategy) +var ( + _ recovery.Strategy = new(Strategy) + _ recovery.AdminHandler = new(Strategy) + _ recovery.PublicHandler = new(Strategy) +) -var _ verification.Strategy = new(Strategy) -var _ verification.AdminHandler = new(Strategy) -var _ verification.PublicHandler = new(Strategy) +var ( + _ verification.Strategy = new(Strategy) + _ verification.AdminHandler = new(Strategy) + _ verification.PublicHandler = new(Strategy) +) type ( // FlowMethod contains the configuration for this selfservice strategy. @@ -83,10 +87,6 @@ func NewStrategy(d strategyDependencies) *Strategy { return &Strategy{d: d, dx: decoderx.NewHTTP()} } -func (s *Strategy) RecoveryNodeGroup() node.UiNodeGroup { - return node.LinkGroup -} - -func (s *Strategy) VerificationNodeGroup() node.UiNodeGroup { +func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.LinkGroup } diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 799b10422c89..40d21eb89764 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -40,7 +40,6 @@ func (s *Strategy) RecoveryStrategyID() string { func (s *Strategy) RegisterPublicRecoveryRoutes(public *x.RouterPublic) { s.d.CSRFHandler().IgnorePath(RouteAdminCreateRecoveryLink) public.POST(RouteAdminCreateRecoveryLink, x.RedirectToAdminRoute(s.d)) - } func (s *Strategy) RegisterAdminRecoveryRoutes(admin *x.RouterAdmin) { @@ -198,7 +197,8 @@ func (s *Strategy) createRecoveryLinkForIdentity(w http.ResponseWriter, r *http. url.Values{ "token": {token.Token}, "flow": {req.ID.String()}, - }).String()}, + }).String(), + }, herodot.UnescapedHTML) } @@ -237,7 +237,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } if len(body.Token) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } @@ -253,7 +253,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return errors.WithStack(flow.ErrCompletedByStrategy) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.RecoveryStrategyID(), body.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), body.Method, s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } @@ -267,11 +267,11 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch req.State { - case recovery.StateChooseMethod: + case flow.StateChooseMethod: fallthrough - case recovery.StateEmailSent: + case flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, req) - case recovery.StatePassedChallenge: + case flow.StatePassedChallenge: // was already handled, do not allow retry return s.retryRecoveryFlowWithMessage(w, r, req.Type, text.NewErrorValidationRecoveryRetrySuccess()) default: @@ -281,7 +281,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, f *recovery.Flow, id *identity.Identity) error { f.UI.Messages.Clear() - f.State = recovery.StatePassedChallenge + f.State = flow.StatePassedChallenge f.SetCSRFToken(s.d.CSRFHandler().RegenerateToken(w, r)) f.RecoveredIdentityID = uuid.NullUUID{ UUID: id.ID, @@ -455,8 +455,8 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.Active = sqlxx.NullString(s.RecoveryNodeGroup()) - f.State = recovery.StateEmailSent + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewRecoveryEmailSent()) if err := s.d.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { return s.HandleRecoveryError(w, r, f, body, err) diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 6dee51cf9207..bfd3dcf57f84 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -61,9 +61,10 @@ func init() { } func createIdentityToRecover(t *testing.T, reg *driver.RegistryDefault, email string) *identity.Identity { - var id = &identity.Identity{ + id := &identity.Identity{ Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}}, + "password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)), SchemaID: config.DefaultIdentityTraitsSchemaID, } @@ -273,7 +274,7 @@ func TestRecovery(t *testing.T) { public, _, publicRouter, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) - var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { if hc == nil { hc = testhelpers.NewDebugClient(t) if !isAPI { @@ -286,11 +287,11 @@ func TestRecovery(t *testing.T) { testhelpers.ExpectURL(isAPI || isSPA, public.URL+recovery.RouteSubmitFlow, conf.SelfServiceFlowRecoveryUI(ctx).String())) } - var expectValidationError = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) } - var expectSuccess = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, http.StatusOK) } @@ -311,14 +312,14 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should require an email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, "Property email is missing.", gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Del("email") } @@ -336,14 +337,14 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should require a valid email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string, value string) { + check := func(t *testing.T, actual string, value string) { assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value), gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -422,16 +423,16 @@ func TestRecovery(t *testing.T) { conf.Set(ctx, config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients, false) }) var email string - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Account access attempted") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Account access attempted") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -452,11 +453,11 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should not be able to recover an inactive account", func(t *testing.T) { - var check = func(t *testing.T, recoverySubmissionResponse, recoveryEmail string, isAPI bool) { + check := func(t *testing.T, recoverySubmissionResponse, recoveryEmail string, isAPI bool) { addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) assert.NoError(t, err) - recoveryLink := testhelpers.CourierExpectLinkInMessage(t, testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account"), 1) + recoveryLink := testhelpers.CourierExpectLinkInMessage(t, testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account"), 1) cl := testhelpers.NewClientWithCookies(t) // Deactivate the identity @@ -503,7 +504,7 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should recover an account", func(t *testing.T) { - var check = func(t *testing.T, recoverySubmissionResponse, recoveryEmail, returnTo string) { + check := func(t *testing.T, recoverySubmissionResponse, recoveryEmail, returnTo string) { addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) assert.NoError(t, err) assert.False(t, addr.Verified) @@ -515,7 +516,7 @@ func TestRecovery(t *testing.T) { require.Len(t, gjson.Get(recoverySubmissionResponse, "ui.messages").Array(), 1, "%s", recoverySubmissionResponse) assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(recoverySubmissionResponse, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by clicking the following link") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -634,8 +635,8 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should recover an account and set the csrf cookies", func(t *testing.T) { - var check = func(t *testing.T, actual, recoveryEmail string, cl *http.Client, do func(*http.Client, *http.Request) (*http.Response, error)) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual, recoveryEmail string, cl *http.Client, do func(*http.Client, *http.Request) (*http.Response, error)) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -659,21 +660,21 @@ func TestRecovery(t *testing.T) { body := x.MustReadAll(actualRes.Body) require.NoError(t, actualRes.Body.Close()) assert.Equal(t, http.StatusOK, actualRes.StatusCode, "%s", body) - assert.Equal(t, string(recovery.StatePassedChallenge), gjson.GetBytes(body, "state").String(), "%s", body) + assert.Equal(t, string(flow.StatePassedChallenge), gjson.GetBytes(body, "state").String(), "%s", body) } email := x.NewUUID().String() + "@ory.sh" id := createIdentityToRecover(t, reg, email) t.Run("case=unauthenticated", func(t *testing.T) { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } check(t, expectSuccess(t, nil, false, false, values), email, testhelpers.NewClientWithCookies(t), (*http.Client).Do) }) t.Run("case=already logged into another account", func(t *testing.T) { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -684,7 +685,7 @@ func TestRecovery(t *testing.T) { }) t.Run("case=already logged into the account", func(t *testing.T) { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -715,8 +716,8 @@ func TestRecovery(t *testing.T) { require.NoError(t, err) assert.True(t, actualSession.IsActive()) - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -736,7 +737,7 @@ func TestRecovery(t *testing.T) { assert.False(t, actualSession.IsActive()) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", recoveryEmail) } @@ -797,7 +798,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") assert.Contains(t, message.Body, "please recover access to your account by clicking the following link") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index e09ffb39f603..804219531d8c 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -122,14 +122,14 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } if len(body.Token) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { return s.handleVerificationError(w, r, nil, body, err) } return s.verificationUseToken(w, r, body, f) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.VerificationStrategyID(), body.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), body.Method, s.d); err != nil { return s.handleVerificationError(w, r, f, body, err) } @@ -138,12 +138,12 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } switch f.State { - case verification.StateChooseMethod: + case flow.StateChooseMethod: fallthrough - case verification.StateEmailSent: + case flow.StateEmailSent: // Do nothing (continue with execution after this switch statement) return s.verificationHandleFormSubmission(w, r, f) - case verification.StatePassedChallenge: + case flow.StatePassedChallenge: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) default: return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) @@ -151,7 +151,7 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *http.Request, f *verification.Flow) error { - var body = new(verificationSubmitPayload) + body := new(verificationSubmitPayload) body, err := s.decodeVerification(r) if err != nil { return s.handleVerificationError(w, r, f, body, err) @@ -178,8 +178,8 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.Active = sqlxx.NullString(s.VerificationNodeGroup()) - f.State = verification.StateEmailSent + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewVerificationEmailSent()) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { return s.handleVerificationError(w, r, f, body, err) @@ -232,7 +232,7 @@ func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, Action: returnTo.String(), } f.UI.Messages.Clear() - f.State = verification.StatePassedChallenge + f.State = flow.StatePassedChallenge // See https://github.com/ory/kratos/issues/1547 f.SetCSRFToken(flow.GetCSRFToken(s.d, w, r, f.Type)) f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) @@ -304,7 +304,6 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http } func (s *Strategy) SendVerificationEmail(ctx context.Context, f *verification.Flow, i *identity.Identity, a *identity.VerifiableAddress) error { - token := NewSelfServiceVerificationToken(a, f, s.d.Config().SelfServiceLinkMethodLifespan(ctx)) if err := s.d.VerificationTokenPersister().CreateVerificationToken(ctx, token); err != nil { return err diff --git a/selfservice/strategy/link/strategy_verification_test.go b/selfservice/strategy/link/strategy_verification_test.go index 474107292f2f..c81834e32b91 100644 --- a/selfservice/strategy/link/strategy_verification_test.go +++ b/selfservice/strategy/link/strategy_verification_test.go @@ -41,15 +41,16 @@ func TestVerification(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) initViper(t, conf) - var identityToVerify = &identity.Identity{ + identityToVerify := &identity.Identity{ ID: x.NewUUID(), Traits: identity.Traits(`{"email":"verifyme@ory.sh"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", Identifiers: []string{"recoverme@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}}, + "password": {Type: "password", Identifiers: []string{"recoverme@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}, + }, } - var verificationEmail = gjson.GetBytes(identityToVerify.Traits, "email").String() + verificationEmail := gjson.GetBytes(identityToVerify.Traits, "email").String() _ = testhelpers.NewVerificationUIFlowEchoServer(t, reg) _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) @@ -62,7 +63,7 @@ func TestVerification(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToVerify, identity.ManagerAllowWriteProtectedTraits)) - var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { + expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string { if hc == nil { hc = testhelpers.NewDebugClient(t) if !isAPI { @@ -75,11 +76,11 @@ func TestVerification(t *testing.T) { testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String())) } - var expectValidationError = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK)) } - var expectSuccess = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { + expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string { return expect(t, hc, isAPI, isSPA, values, http.StatusOK) } @@ -114,14 +115,14 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require an email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, "Property email is missing.", gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), "%s", actual) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Del("email") } @@ -139,7 +140,7 @@ func TestVerification(t *testing.T) { }) t.Run("description=should require a valid email to be sent", func(t *testing.T) { - var check = func(t *testing.T, actual string, value string) { + check := func(t *testing.T, actual string, value string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value), gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(), @@ -147,7 +148,7 @@ func TestVerification(t *testing.T) { } for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} { - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -172,16 +173,16 @@ func TestVerification(t *testing.T) { conf.Set(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, false) }) var email string - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, email, "Someone tried to verify this email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Someone tried to verify this email address") assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", email) } @@ -245,14 +246,14 @@ func TestVerification(t *testing.T) { v.Set("email", verificationEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "Hi, please verify your account by clicking the following link") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) time.Sleep(time.Millisecond * 201) - //Clear cookies as link might be opened in another browser + // Clear cookies as link might be opened in another browser c = testhelpers.NewClientWithCookies(t) res, err := c.Get(verificationLink) require.NoError(t, err) @@ -269,12 +270,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw)) - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") assert.Contains(t, message.Body, "please verify your account by clicking the following link") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) @@ -304,7 +305,7 @@ func TestVerification(t *testing.T) { assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now())) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } @@ -322,8 +323,8 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address when the link is opened in another browser", func(t *testing.T) { - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, verificationEmail, "Please verify your email address") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address") verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -344,7 +345,7 @@ func TestVerification(t *testing.T) { assert.EqualValues(t, "passed_challenge", gjson.Get(actualBody, "state").String()) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", verificationEmail) } @@ -354,7 +355,7 @@ func TestVerification(t *testing.T) { newValidFlow := func(t *testing.T, fType flow.Type, requestURL string) (*verification.Flow, *link.VerificationToken) { f, err := verification.NewFlow(conf, time.Hour, x.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), nil, fType) require.NoError(t, err) - f.State = verification.StateEmailSent + f.State = flow.StateEmailSent require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID) identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email) @@ -412,7 +413,6 @@ func TestVerification(t *testing.T) { }) t.Run("case=should not be able to use code from different flow", func(t *testing.T) { - f1, _ := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow) _, t2 := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow) diff --git a/selfservice/strategy/lookup/login.go b/selfservice/strategy/lookup/login.go index b5e5fddda7e8..75edc2181059 100644 --- a/selfservice/strategy/lookup/login.go +++ b/selfservice/strategy/lookup/login.go @@ -94,7 +94,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return nil, err } diff --git a/selfservice/strategy/lookup/settings.go b/selfservice/strategy/lookup/settings.go index 261336ecdcbc..6f61966e353c 100644 --- a/selfservice/strategy/lookup/settings.go +++ b/selfservice/strategy/lookup/settings.go @@ -108,7 +108,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. if p.RegenerateLookup || p.RevealLookup || p.ConfirmLookup || p.DisableLookup { // This method has only two submit buttons p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) } } else { @@ -141,7 +141,7 @@ func (s *Strategy) continueSettingsFlow( ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod, ) error { if p.ConfirmLookup || p.RevealLookup || p.RegenerateLookup || p.DisableLookup { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return err } diff --git a/selfservice/strategy/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go index 92a81e964971..fce2be4c0974 100644 --- a/selfservice/strategy/lookup/settings_test.go +++ b/selfservice/strategy/lookup/settings_test.go @@ -273,7 +273,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=can not confirm without regenerate", func(t *testing.T) { id, codes := createIdentity(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.LookupConfirm, "true") } @@ -310,7 +310,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=regenerate but no confirmation", func(t *testing.T) { id, codes := createIdentity(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.LookupRegenerate, "true") } @@ -363,13 +363,13 @@ func TestCompleteSettings(t *testing.T) { }, } { t.Run("credentials="+tc.d, func(t *testing.T) { - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Del(node.LookupReveal) v.Del(node.LookupDisable) v.Set(node.LookupRegenerate, "true") } - var payloadConfirm = func(v url.Values) { + payloadConfirm := func(v url.Values) { v.Del(node.LookupRegenerate) v.Del(node.LookupDisable) v.Del(node.LookupReveal) @@ -401,7 +401,7 @@ func TestCompleteSettings(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, apiClient, publicTS, "aal2", string(identity.CredentialsTypeLookup)) @@ -427,7 +427,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, browserClient, publicTS, "aal2", string(identity.CredentialsTypeLookup)) } @@ -463,7 +463,7 @@ func TestCompleteSettings(t *testing.T) { }, } { t.Run("credentials="+tc.d, func(t *testing.T) { - var payloadConfirm = func(v url.Values) { + payloadConfirm := func(v url.Values) { v.Del(node.LookupRegenerate) v.Del(node.LookupReveal) v.Set(node.LookupDisable, "true") @@ -489,7 +489,7 @@ func TestCompleteSettings(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, apiClient, publicTS, "aal1") @@ -512,7 +512,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) checkIdentity(t, id, f) testhelpers.EnsureAAL(t, browserClient, publicTS, "aal1") } diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 2639868275c1..1b4f9ca56034 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -146,12 +146,15 @@ func generateState(flowID string) *State { Data: x.NewUUID().Bytes(), } } + func (s *State) setCode(code string) { s.Data = sha512.New().Sum([]byte(code)) } + func (s *State) codeMatches(code string) bool { return bytes.Equal(s.Data, sha512.New().Sum([]byte(code))) } + func parseState(s string) (*State, error) { raw, err := base64.RawURLEncoding.DecodeString(s) if err != nil { diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 23f4ff60514a..24af92562f5c 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -167,12 +167,12 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleError(w, r, f, "", nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP form request: %s", err.Error()))) } - var pid = p.Provider // this can come from both url query and post body + pid := p.Provider // this can come from both url query and post body if pid == "" { return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return nil, s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 1e303c50df35..690d4650fa3a 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -127,12 +127,12 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat f.TransientPayload = p.TransientPayload - var pid = p.Provider // this can come from both url query and post body + pid := p.Provider // this can come from both url query and post body if pid == "" { return errors.WithStack(flow.ErrStrategyNotResponsible) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index d7497e567ab3..69f2bc03a560 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -76,41 +76,59 @@ func TestSettingsStrategy(t *testing.T) { // Make test data for this test run unique testID := x.NewUUID().String() users := map[string]*identity.Identity{ - "password": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"john` + testID + `@doe.com"}`), + "password": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"john` + testID + `@doe.com"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", + "password": { + Type: "password", Identifiers: []string{"john+" + testID + "@doe.com"}, - Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`)}}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + }, }, - "oryer": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+` + testID + `@ory.sh"}`), + "oryer": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+` + testID + `@ory.sh"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+` + testID + `"}]}`), + }, + }, }, - "githuber": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+github+` + testID + `@ory.sh"}`), + "githuber": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+github+` + testID + `@ory.sh"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+github+" + testID, "github:hackerman+github+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+github+` + testID + `"},{"provider":"github","subject":"hackerman+github+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+github+` + testID + `"},{"provider":"github","subject":"hackerman+github+` + testID + `"}]}`), + }, + }, SchemaID: config.DefaultIdentityTraitsSchemaID, }, - "multiuser": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+multiuser+` + testID + `@ory.sh"}`), + "multiuser": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+multiuser+` + testID + `@ory.sh"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", + "password": { + Type: "password", Identifiers: []string{"hackerman+multiuser+" + testID + "@ory.sh"}, - Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`)}, - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+multiuser+" + testID, "google:hackerman+multiuser+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+multiuser+` + testID + `"},{"provider":"google","subject":"hackerman+multiuser+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+multiuser+` + testID + `"},{"provider":"google","subject":"hackerman+multiuser+` + testID + `"}]}`), + }, + }, SchemaID: config.DefaultIdentityTraitsSchemaID, }, } agents := testhelpers.AddAndLoginIdentities(t, reg, publicTS, users) - var newProfileFlow = func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *settings.Flow { + newProfileFlow := func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *settings.Flow { req, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), x.ParseUUID(string(testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS).Id))) require.NoError(t, err) @@ -131,7 +149,7 @@ func TestSettingsStrategy(t *testing.T) { } // does the same as new profile request but uses the SDK - var nprSDK = func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *kratos.SettingsFlow { + nprSDK := func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *kratos.SettingsFlow { return testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS) } @@ -208,11 +226,11 @@ func TestSettingsStrategy(t *testing.T) { } }) - var action = func(req *kratos.SettingsFlow) string { + action := func(req *kratos.SettingsFlow) string { return req.Ui.Action } - var checkCredentials = func(t *testing.T, shouldExist bool, iid uuid.UUID, provider, subject string, expectTokens bool) { + checkCredentials := func(t *testing.T, shouldExist bool, iid uuid.UUID, provider, subject string, expectTokens bool) { actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), iid) require.NoError(t, err) @@ -242,7 +260,7 @@ func TestSettingsStrategy(t *testing.T) { require.EqualValues(t, shouldExist, found) } - var reset = func(t *testing.T) func() { + reset := func(t *testing.T) func() { return func() { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Minute*5) agents = testhelpers.AddAndLoginIdentities(t, reg, publicTS, users) @@ -250,20 +268,20 @@ func TestSettingsStrategy(t *testing.T) { } t.Run("suite=unlink", func(t *testing.T) { - var unlink = func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + unlink := func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { req = nprSDK(t, agents[agent], "", time.Hour) body, res = testhelpers.HTTPPostForm(t, agents[agent], action(req), &url.Values{"csrf_token": {x.FakeCSRFToken}, "unlink": {provider}}) return } - var unlinkInvalid = func(agent, provider, errorMessage string) func(t *testing.T) { + unlinkInvalid := func(agent, provider, errorMessage string) func(t *testing.T) { return func(t *testing.T) { body, res, req := unlink(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) - //assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) + // assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) // The original options to link google and github are still there t.Run("flow=fetch", func(t *testing.T) { @@ -302,7 +320,7 @@ func TestSettingsStrategy(t *testing.T) { t.Run("case=should not be able to unlink a connection without a privileged session", func(t *testing.T) { agent, provider := "githuber", "github" - var runUnauthed = func(t *testing.T) *kratos.SettingsFlow { + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) time.Sleep(time.Millisecond) t.Cleanup(reset(t)) @@ -311,7 +329,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateShowForm, rs.State) + require.EqualValues(t, flow.StateShowForm, rs.State) checkCredentials(t, true, users[agent].ID, provider, "hackerman+github+"+testID, false) @@ -340,19 +358,19 @@ func TestSettingsStrategy(t *testing.T) { }) t.Run("suite=link", func(t *testing.T) { - var link = func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + link := func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { req = nprSDK(t, agents[agent], "", time.Hour) body, res = testhelpers.HTTPPostForm(t, agents[agent], action(req), &url.Values{"csrf_token": {x.FakeCSRFToken}, "link": {provider}}) return } - var linkInvalid = func(agent, provider string) func(t *testing.T) { + linkInvalid := func(agent, provider string) func(t *testing.T) { return func(t *testing.T) { body, res, req := link(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) - //assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) + // assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), publicTS.URL+settings.RouteSubmitFlow+"?flow=") // The original options to link google and github are still there @@ -427,7 +445,7 @@ func TestSettingsStrategy(t *testing.T) { updatedFlowSDK, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(originalFlow.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateSuccess, updatedFlowSDK.State) + require.EqualValues(t, flow.StateSuccess, updatedFlowSDK.State) t.Run("flow=original", func(t *testing.T) { snapshotx.SnapshotTExcept(t, originalFlow.Ui.Nodes, []string{"0.attributes.value", "1.attributes.value"}) @@ -454,7 +472,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateSuccess, rs.State) + require.EqualValues(t, flow.StateSuccess, rs.State) snapshotx.SnapshotTExcept(t, rs.Ui.Nodes, []string{"0.attributes.value", "1.attributes.value"}) @@ -529,7 +547,7 @@ func TestSettingsStrategy(t *testing.T) { agent, provider := "githuber", "google" subject = "hackerman+new+google+" + testID - var runUnauthed = func(t *testing.T) *kratos.SettingsFlow { + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) time.Sleep(time.Millisecond) t.Cleanup(reset(t)) @@ -538,7 +556,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateShowForm, rs.State) + require.EqualValues(t, flow.StateShowForm, rs.State) checkCredentials(t, false, users[agent].ID, provider, subject, true) @@ -675,10 +693,12 @@ func TestPopulateSettingsMethod(t *testing.T) { oidc.NewUnlinkNode("google"), }, withpw: true, - i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ - "google:1234", + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + }, + Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`), }, - Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`)}, }, { c: defaultConfig, @@ -688,11 +708,13 @@ func TestPopulateSettingsMethod(t *testing.T) { oidc.NewUnlinkNode("google"), oidc.NewUnlinkNode("facebook"), }, - i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ - "google:1234", - "facebook:1234", + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + "facebook:1234", + }, + Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), }, - Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`)}, }, } { t.Run("iteration="+strconv.Itoa(k), func(t *testing.T) { diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 30010b80eb54..7a56ebaf45d4 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -51,7 +51,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return nil, err } diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 57ebca990923..ac5c91789745 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -78,7 +78,7 @@ func (s *Strategy) decode(p *UpdateRegistrationFlowWithPasswordMethod, r *http.R } func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return err } diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index 45e6bc045650..4ba0b115e53a 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -75,7 +75,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) } @@ -109,7 +109,7 @@ func (s *Strategy) continueSettingsFlow( w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasswordMethod, ) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go index 1e64544edee1..2993c428b701 100644 --- a/selfservice/strategy/password/strategy.go +++ b/selfservice/strategy/password/strategy.go @@ -26,9 +26,11 @@ import ( "github.com/ory/kratos/x" ) -var _ login.Strategy = new(Strategy) -var _ registration.Strategy = new(Strategy) -var _ identity.ActiveCredentialsCounter = new(Strategy) +var ( + _ login.Strategy = new(Strategy) + _ registration.Strategy = new(Strategy) + _ identity.ActiveCredentialsCounter = new(Strategy) +) type registrationStrategyDependencies interface { x.LoggingProvider diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go index 5c4a68be9a78..e94d779aef61 100644 --- a/selfservice/strategy/profile/strategy.go +++ b/selfservice/strategy/profile/strategy.go @@ -116,7 +116,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, &p, err) } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { return ctxUpdate, err } @@ -144,7 +144,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. } func (s *Strategy) continueFlow(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithProfileMethod) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go index bb09a2b925a0..f67407fe799f 100644 --- a/selfservice/strategy/profile/strategy_test.go +++ b/selfservice/strategy/profile/strategy_test.go @@ -32,6 +32,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/x" "github.com/ory/x/assertx" @@ -189,7 +190,7 @@ func TestStrategyTraits(t *testing.T) { t.Run("description=hydrate the proper fields", func(t *testing.T) { setPrivileged(t) - var run = func(t *testing.T, id *identity.Identity, payload *kratos.SettingsFlow, route string) { + run := func(t *testing.T, id *identity.Identity, payload *kratos.SettingsFlow, route string) { assert.NotEmpty(t, payload.Identity) assert.Equal(t, id.ID.String(), string(payload.Identity.Id)) assert.JSONEq(t, string(id.Traits), x.MustEncodeJSON(t, payload.Identity.Traits)) @@ -230,7 +231,7 @@ func TestStrategyTraits(t *testing.T) { }) }) - var expectValidationError = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + expectValidationError := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String())) @@ -239,7 +240,7 @@ func TestStrategyTraits(t *testing.T) { t.Run("description=should come back with form errors if some profile data is invalid", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.NotEmpty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", actual) assert.Equal(t, "too-short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual) assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) @@ -247,7 +248,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "length must be >= 25, but got 9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual) } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", "profile") v.Set("traits.should_long_string", "too-short") v.Set("traits.stringy", "bazbar") @@ -298,7 +299,7 @@ func TestStrategyTraits(t *testing.T) { }) t.Run("description=should end up at the login endpoint if trying to update protected field without sudo mode", func(t *testing.T) { - var run = func(t *testing.T, config *kratos.SettingsFlow, isAPI bool, c *http.Client) *http.Response { + run := func(t *testing.T, config *kratos.SettingsFlow, isAPI bool, c *http.Client) *http.Response { time.Sleep(time.Millisecond) values := testhelpers.SDKFormFieldsToURLValues(config.Ui.Nodes) @@ -343,7 +344,7 @@ func TestStrategyTraits(t *testing.T) { defer res.Body.Close() assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body) - assert.EqualValues(t, settings.StateSuccess, gjson.GetBytes(body, "state").String(), "%s", body) + assert.EqualValues(t, flow.StateSuccess, gjson.GetBytes(body, "state").String(), "%s", body) }) }) }) @@ -351,14 +352,14 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=fail first update", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) assert.Equal(t, "1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual) assert.Equal(t, "must be >= 1200 but found 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual) assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.should_big_number", "1") } @@ -379,8 +380,8 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=fail second update", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual) @@ -394,7 +395,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Del("traits.should_big_number") v.Set("traits.should_long_string", "short") @@ -414,7 +415,7 @@ func TestStrategyTraits(t *testing.T) { }) }) - var expectSuccess = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + expectSuccess := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values, http.StatusOK, testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String())) @@ -423,8 +424,8 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=succeed with final request", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual) @@ -435,7 +436,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "this is such a long string, amazing stuff!", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").Value(), "%s", actual) } - var payload = func(newEmail string) func(v url.Values) { + payload := func(newEmail string) func(v url.Values) { return func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.email", newEmail) @@ -463,11 +464,11 @@ func TestStrategyTraits(t *testing.T) { t.Run("flow=try another update with invalid data", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, actual string) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual string) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual) } - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.should_long_string", "short") } @@ -526,8 +527,8 @@ func TestStrategyTraits(t *testing.T) { conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, settings.StrategyProfile), nil) }) - var check = func(t *testing.T, actual, newEmail string) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, actual, newEmail string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual) m, err := reg.CourierPersister().LatestQueuedMessage(context.Background()) @@ -535,7 +536,7 @@ func TestStrategyTraits(t *testing.T) { assert.Contains(t, m.Subject, "verify your email address") } - var payload = func(newEmail string) func(v url.Values) { + payload := func(newEmail string) func(v url.Values) { return func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.email", newEmail) @@ -564,8 +565,8 @@ func TestStrategyTraits(t *testing.T) { t.Run("description=should update protected field with sudo mode", func(t *testing.T) { setPrivileged(t) - var check = func(t *testing.T, newEmail string, actual string) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) + check := func(t *testing.T, newEmail string, actual string) { + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual) @@ -573,7 +574,7 @@ func TestStrategyTraits(t *testing.T) { assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here } - var payload = func(email string) func(v url.Values) { + payload := func(email string) func(v url.Values) { return func(v url.Values) { v.Set("method", settings.StrategyProfile) v.Set("traits.email", email) diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go index e3840eba1b9b..bc9816d265f3 100644 --- a/selfservice/strategy/totp/login.go +++ b/selfservice/strategy/totp/login.go @@ -90,7 +90,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return nil, err } diff --git a/selfservice/strategy/totp/settings.go b/selfservice/strategy/totp/settings.go index 928c288be365..0587e87e2d6a 100644 --- a/selfservice/strategy/totp/settings.go +++ b/selfservice/strategy/totp/settings.go @@ -94,10 +94,10 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. if p.UnlinkTOTP { // This is a submit so we need to manually set the type to TOTP p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) } - } else if err := flow.MethodEnabledAndAllowedFromRequest(r, s.SettingsStrategyID(), s.d); err != nil { + } else if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) } @@ -127,7 +127,7 @@ func (s *Strategy) continueSettingsFlow( w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithTotpMethod, ) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } diff --git a/selfservice/strategy/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go index b9b294ffdf4e..0fd479f1b220 100644 --- a/selfservice/strategy/totp/settings_test.go +++ b/selfservice/strategy/totp/settings_test.go @@ -148,7 +148,7 @@ func TestCompleteSettings(t *testing.T) { }) id, _, key := createIdentity(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("totp_unlink", "true") } @@ -190,7 +190,7 @@ func TestCompleteSettings(t *testing.T) { }) id := createIdentityWithoutTOTP(t, reg) - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.TOTPCode, "111111") } @@ -225,7 +225,7 @@ func TestCompleteSettings(t *testing.T) { }) t.Run("type=unlink TOTP device", func(t *testing.T) { - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set("totp_unlink", "true") } @@ -239,7 +239,7 @@ func TestCompleteSettings(t *testing.T) { actual, res := doAPIFlow(t, payload, id) assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) }) @@ -248,7 +248,7 @@ func TestCompleteSettings(t *testing.T) { actual, res := doBrowserFlow(t, true, payload, id) assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) }) @@ -257,13 +257,13 @@ func TestCompleteSettings(t *testing.T) { actual, res := doBrowserFlow(t, false, payload, id) assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), uiTS.URL) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) }) }) t.Run("type=set up TOTP device but code is incorrect", func(t *testing.T) { - var payload = func(v url.Values) { + payload := func(v url.Values) { v.Set(node.TOTPCode, "111111") } @@ -332,10 +332,10 @@ func TestCompleteSettings(t *testing.T) { if isAPI || isSPA { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) - assert.EqualValues(t, settings.StateSuccess, gjson.Get(actual, "state").String(), actual) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) } actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id)) diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index 815e99cb456b..342c8a3ab76f 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -82,5 +82,6 @@ ] }, "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index 815e99cb456b..342c8a3ab76f 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -82,5 +82,6 @@ ] }, "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 58cf7b0f25a1..200fdcea2280 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -211,7 +211,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, flow.ErrStrategyNotResponsible } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleLoginError(r, f, err) } diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go index fab8e2cb77c5..565125319634 100644 --- a/selfservice/strategy/webauthn/registration.go +++ b/selfservice/strategy/webauthn/registration.go @@ -113,7 +113,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return s.handleRegistrationError(w, r, f, &p, err) } diff --git a/selfservice/strategy/webauthn/settings.go b/selfservice/strategy/webauthn/settings.go index ad00289c010a..51fbcf0f5adb 100644 --- a/selfservice/strategy/webauthn/settings.go +++ b/selfservice/strategy/webauthn/settings.go @@ -112,7 +112,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. if len(p.Register+p.Remove) > 0 { // This method has only two submit buttons p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) } } else { @@ -146,7 +146,7 @@ func (s *Strategy) continueSettingsFlow( ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod, ) error { if len(p.Register+p.Remove) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return err } diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index 04f571e7ab5e..27413df38b16 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -334,7 +334,7 @@ func TestCompleteSettings(t *testing.T) { } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) require.NoError(t, err) @@ -386,7 +386,7 @@ func TestCompleteSettings(t *testing.T) { } t.Run("response", func(t *testing.T) { - assert.EqualValues(t, settings.StateShowForm, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateShowForm, gjson.Get(body, "state").String(), body) snapshotx.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes.#(attributes.name==webauthn_remove)").String()), nil) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) @@ -426,7 +426,7 @@ func TestCompleteSettings(t *testing.T) { } t.Run("response", func(t *testing.T) { - assert.EqualValues(t, settings.StateSuccess, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) require.NoError(t, err) _, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) @@ -463,7 +463,7 @@ func TestCompleteSettings(t *testing.T) { } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, gjson.Get(body, "state").String(), body) + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) } actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) @@ -496,7 +496,7 @@ func TestCompleteSettings(t *testing.T) { } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } - assert.EqualValues(t, settings.StateSuccess, json.RawMessage(gjson.Get(body, "state").String())) + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(body, "state").String())) actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) require.NoError(t, err) diff --git a/spec/api.json b/spec/api.json old mode 100755 new mode 100644 index b63ad4a5a91c..6f6ef818ab5b --- a/spec/api.json +++ b/spec/api.json @@ -81,6 +81,9 @@ } }, "schemas": { + "CodeAddressType": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -95,6 +98,15 @@ "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger.", "type": "object" }, + "LoginFlowState": { + "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "Login Flow State" + }, "NullBool": { "nullable": true, "type": "boolean" @@ -399,6 +411,32 @@ "title": "RecoveryAddressType must not exceed 16 characters as that is the limitation in the SQL Schema.", "type": "string" }, + "RecoveryFlowState": { + "description": "The state represents the state of the recovery flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "Recovery Flow State" + }, + "RegistrationFlowState": { + "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "State represents the state of this request:" + }, + "SettingsFlowState": { + "description": "show_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent.", + "enum": [ + "show_form", + "success" + ], + "title": "State represents the state of this flow. It knows two states:" + }, "Time": { "format": "date-time", "type": "string" @@ -407,6 +445,15 @@ "format": "uuid4", "type": "string" }, + "VerificationFlowState": { + "description": "The state represents the state of the verification flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "Verification Flow State" + }, "authenticatorAssuranceLevel": { "description": "The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder\nfor an attacker to compromise the account.\n\nGenerally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g.\npassword + TOTP) have been used.\n\nTo learn more about these levels please head over to: https://www.ory.sh/kratos/docs/concepts/credentials", "enum": [ @@ -883,6 +930,18 @@ }, "type": "object" }, + "identityCredentialsOTP": { + "description": "CredentialsOTP represents an OTP code", + "properties": { + "address_type": { + "$ref": "#/components/schemas/CodeAddressType" + }, + "used_at": { + "$ref": "#/components/schemas/NullTime" + } + }, + "type": "object" + }, "identityCredentialsOidc": { "properties": { "providers": { @@ -1186,6 +1245,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the login flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -1204,7 +1266,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "title": "Login Flow", "type": "object" @@ -1262,7 +1325,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "enum": [ "recovery_invalid", "recovery_valid", @@ -1273,10 +1336,12 @@ "verification_code_invalid", "verification_code_valid", "otp", - "stub" + "stub", + "login_code_valid", + "registration_code_valid" ], "type": "string", - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/components/schemas/courierMessageType" @@ -1481,7 +1546,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/recoveryFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -1502,16 +1567,6 @@ "title": "A Recovery Flow", "type": "object" }, - "recoveryFlowState": { - "description": "The state represents the state of the recovery flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "enum": [ - "choose_method", - "sent_email", - "passed_challenge" - ], - "title": "Recovery Flow State", - "type": "string" - }, "recoveryIdentityAddress": { "properties": { "created_at": { @@ -1600,6 +1655,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed." + }, "transient_payload": { "description": "TransientPayload is used to pass data from the registration to a webhook", "type": "object" @@ -1617,7 +1675,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "type": "object" }, @@ -1806,7 +1865,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/settingsFlowState" + "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -1828,15 +1887,6 @@ "title": "Flow represents a Settings Flow", "type": "object" }, - "settingsFlowState": { - "description": "show_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent.", - "enum": [ - "show_form", - "success" - ], - "title": "State represents the state of this flow. It knows two states:", - "type": "string" - }, "successfulCodeExchangeResponse": { "description": "The Response for Registration Flows via API", "properties": { @@ -2361,6 +2411,36 @@ } ] }, + "updateLoginFlowWithCodeMethod": { + "description": "Update Login flow using the code method", + "properties": { + "code": { + "description": "Code is the 6 digits code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "identifier": { + "description": "Identifier is the code identifier\nThe identifier requires that the user has already completed the registration or settings with code flow.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"code\" when logging in using the code strategy.", + "type": "string" + }, + "resend": { + "description": "Resend is set when the user wants to resend the code", + "type": "string" + } + }, + "required": [ + "method", + "csrf_token" + ], + "type": "object" + }, "updateLoginFlowWithLookupSecretMethod": { "description": "Update Login Flow with Lookup Secret Method", "properties": { @@ -2589,6 +2669,40 @@ } ] }, + "updateRegistrationFlowWithCodeMethod": { + "description": "Update Registration Flow with Code Method", + "properties": { + "code": { + "description": "The OTP Code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "The CSRF Token", + "type": "string" + }, + "method": { + "description": "Method to use\n\nThis field must be set to `code` when using the code method.", + "type": "string" + }, + "resend": { + "description": "Resend restarts the flow with a new code", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + }, + "required": [ + "traits", + "method" + ], + "type": "object" + }, "updateRegistrationFlowWithOidcMethod": { "description": "Update Registration Flow with OpenID Connect Method", "properties": { @@ -3041,7 +3155,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/verificationFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -3059,16 +3173,6 @@ "title": "A Verification Flow", "type": "object" }, - "verificationFlowState": { - "description": "The state represents the state of the verification flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "enum": [ - "choose_method", - "sent_email", - "passed_challenge" - ], - "title": "Verification Flow State", - "type": "string" - }, "version": { "properties": { "version": { diff --git a/spec/swagger.json b/spec/swagger.json index cad0b006024e..b8f97ef8d9d1 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3082,6 +3082,9 @@ } }, "definitions": { + "CodeAddressType": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -3096,6 +3099,24 @@ "type": "object", "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, + "LoginFlowState": { + "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", + "title": "Login Flow State" + }, + "NullTime": { + "description": "NullTime implements the Scanner interface so\nit can be used as a scan destination, similar to NullString.", + "type": "object", + "title": "NullTime represents a time.Time that may be null.", + "properties": { + "Time": { + "type": "string", + "format": "date-time" + }, + "Valid": { + "type": "boolean" + } + } + }, "OAuth2Client": { "type": "object", "title": "OAuth2Client OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.", @@ -3379,7 +3400,23 @@ "type": "string", "title": "RecoveryAddressType must not exceed 16 characters as that is the limitation in the SQL Schema." }, + "RecoveryFlowState": { + "description": "The state represents the state of the recovery flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", + "title": "Recovery Flow State" + }, + "RegistrationFlowState": { + "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", + "title": "State represents the state of this request:" + }, + "SettingsFlowState": { + "description": "show_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent.", + "title": "State represents the state of this flow. It knows two states:" + }, "UUID": {"type": "string", "format": "uuid4"}, + "VerificationFlowState": { + "description": "The state represents the state of the verification flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", + "title": "Verification Flow State" + }, "authenticatorAssuranceLevel": { "description": "The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder\nfor an attacker to compromise the account.\n\nGenerally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g.\npassword + TOTP) have been used.\n\nTo learn more about these levels please head over to: https://www.ory.sh/kratos/docs/concepts/credentials", "type": "string", @@ -3830,6 +3867,18 @@ } } }, + "identityCredentialsOTP": { + "description": "CredentialsOTP represents an OTP code", + "type": "object", + "properties": { + "address_type": { + "$ref": "#/definitions/CodeAddressType" + }, + "used_at": { + "$ref": "#/definitions/NullTime" + } + } + }, "identityCredentialsOidc": { "type": "object", "title": "CredentialsOIDC is contains the configuration for credentials of the type oidc.", @@ -4081,7 +4130,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "properties": { "active": { @@ -4133,6 +4183,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the login flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -4212,7 +4265,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "type": "string", "enum": [ "recovery_invalid", @@ -4224,9 +4277,11 @@ "verification_code_invalid", "verification_code_valid", "otp", - "stub" + "stub", + "login_code_valid", + "registration_code_valid" ], - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/definitions/courierMessageType" @@ -4420,7 +4475,7 @@ "type": "string" }, "state": { - "$ref": "#/definitions/recoveryFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, "type": { "$ref": "#/definitions/selfServiceFlowType" @@ -4430,11 +4485,6 @@ } } }, - "recoveryFlowState": { - "description": "The state represents the state of the recovery flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "type": "string", - "title": "Recovery Flow State" - }, "recoveryIdentityAddress": { "type": "object", "required": [ @@ -4492,7 +4542,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "properties": { "active": { @@ -4532,6 +4583,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed." + }, "transient_payload": { "description": "TransientPayload is used to pass data from the registration to a webhook", "type": "object" @@ -4730,7 +4784,7 @@ "type": "string" }, "state": { - "$ref": "#/definitions/settingsFlowState" + "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, "type": { "$ref": "#/definitions/selfServiceFlowType" @@ -4740,11 +4794,6 @@ } } }, - "settingsFlowState": { - "description": "show_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent.", - "type": "string", - "title": "State represents the state of this flow. It knows two states:" - }, "successfulCodeExchangeResponse": { "description": "The Response for Registration Flows via API", "type": "object", @@ -5218,6 +5267,36 @@ "updateLoginFlowBody": { "type": "object" }, + "updateLoginFlowWithCodeMethod": { + "description": "Update Login flow using the code method", + "type": "object", + "required": [ + "method", + "csrf_token" + ], + "properties": { + "code": { + "description": "Code is the 6 digits code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "identifier": { + "description": "Identifier is the code identifier\nThe identifier requires that the user has already completed the registration or settings with code flow.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"code\" when logging in using the code strategy.", + "type": "string" + }, + "resend": { + "description": "Resend is set when the user wants to resend the code", + "type": "string" + } + } + }, "updateLoginFlowWithLookupSecretMethod": { "description": "Update Login Flow with Lookup Secret Method", "type": "object", @@ -5414,6 +5493,40 @@ "description": "Update Registration Request Body", "type": "object" }, + "updateRegistrationFlowWithCodeMethod": { + "description": "Update Registration Flow with Code Method", + "type": "object", + "required": [ + "traits", + "method" + ], + "properties": { + "code": { + "description": "The OTP Code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "The CSRF Token", + "type": "string" + }, + "method": { + "description": "Method to use\n\nThis field must be set to `code` when using the code method.", + "type": "string" + }, + "resend": { + "description": "Resend restarts the flow with a new code", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, "updateRegistrationFlowWithOidcMethod": { "description": "Update Registration Flow with OpenID Connect Method", "type": "object", @@ -5827,7 +5940,7 @@ "type": "string" }, "state": { - "$ref": "#/definitions/verificationFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, "type": { "$ref": "#/definitions/selfServiceFlowType" @@ -5837,11 +5950,6 @@ } } }, - "verificationFlowState": { - "description": "The state represents the state of the verification flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "type": "string", - "title": "Verification Flow State" - }, "version": { "type": "object", "properties": { diff --git a/test/e2e/.go-version b/test/e2e/.go-version new file mode 100644 index 000000000000..6681c8c19ab4 --- /dev/null +++ b/test/e2e/.go-version @@ -0,0 +1 @@ +1.19.8 diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts new file mode 100644 index 000000000000..5f1cebcfd83d --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -0,0 +1,189 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Login error messages with code method", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "code", + }, + // { + // route: react.login, + // app: "react" as "react", + // profile: "code", + // }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.traits.schema.json", + ) + }) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + + cy.visit(route) + const email = gen.email() + cy.wrap(email).as("email") + cy.registerWithCode({ email }) + + cy.deleteMail() + cy.clearAllCookies() + }) + + it("should show error message when account identifier does not exist", () => { + const email = gen.email() + + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).type(email) + cy.submitCodeForm() + + cy.url().should("contain", "login") + + cy.get('[data-testid="ui/message/4000028"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", + ) + }) + + it("should show error message when code is invalid", () => { + cy.get("@email").then((email) => { + cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') + .clear() + .type(email.toString()) + }) + + cy.submitCodeForm() + + cy.url().should("contain", "login") + cy.get('[data-testid="ui/message/1010014"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( + "invalid-code", + ) + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4010008"]').should( + "contain", + "The login code is invalid or has already been used. Please try again.", + ) + }) + + it("should show error message when identifier has changed", () => { + cy.get("@email").then((email) => { + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).type(email.toString()) + }) + + cy.submitCodeForm() + + cy.url().should("contain", "login") + cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') + .clear() + .type(gen.email()) + cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( + "invalid-code", + ) + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000028"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", + ) + }) + + it("should show error message when code is expired", () => { + cy.clearAllCookies() + + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1ns" + return config + }) + + cy.visit(route) + + cy.get("@email").then((email) => { + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).type(email.toString()) + }) + cy.submitCodeForm() + + cy.url().should("contain", "login") + + cy.get("@email").then((email) => { + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( + code, + ) + cy.submitCodeForm() + }) + }) + + cy.get('[data-testid="ui/message/4040001"]').should( + "contain", + "The login flow expired", + ) + + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1h" + return config + }) + }) + + it("should show error message when required fields are missing", () => { + const email = gen.email() + + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).type(email) + cy.submitCodeForm() + + cy.url().should("contain", "login") + + cy.removeAttribute( + ['form[data-testid="login-flow-code"] input[name="code"]'], + "required", + ) + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property code is missing", + ) + + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).clear() + cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( + "invalid-code", + ) + cy.removeAttribute( + ['form[data-testid="login-flow-code"] input[name="identifier"]'], + "required", + ) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property email is missing", + ) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts new file mode 100644 index 000000000000..efd554eef982 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -0,0 +1,161 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration error messages with code method", () => { + ;[ + { + route: express.registration, + login: express.login, + app: "express" as "express", + profile: "code", + }, + // { + // route: react.registration, + // app: "react" as "react", + // profile: "code", + // }, + ].forEach(({ route, login, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.traits.schema.json", + ) + }) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should show error message when code is invalid", () => { + const email = gen.email() + + cy.get( + 'form[data-testid="registration-flow-code"] input[name="traits.email"]', + ).type(email) + cy.submitCodeForm() + + cy.url().should("contain", "registration") + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.get( + 'form[data-testid="registration-flow-code"] input[name="code"]', + ).type("invalid-code") + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4040003"]').should( + "contain", + "The registration code is invalid or has already been used. Please try again.", + ) + }) + + it("should show error message when traits have changed", () => { + const email = gen.email() + + cy.get( + 'form[data-testid="registration-flow-code"] input[name="traits.email"]', + ).type(email) + cy.submitCodeForm() + + cy.url().should("contain", "registration") + cy.get( + 'form[data-testid="registration-flow-code"] input[name="traits.email"]', + ) + .clear() + .type("changed-email@email.com") + cy.get( + 'form[data-testid="registration-flow-code"] input[name="code"]', + ).type("invalid-code") + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000029"]').should( + "contain", + "The provided traits do not match the traits previously associated with this flow.", + ) + }) + + it("should show error message when code is expired", () => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1ns" + return config + }) + + const email = gen.email() + + cy.get( + 'form[data-testid="registration-flow-code"] input[name="traits.email"]', + ).type(email) + cy.submitCodeForm() + + cy.url().should("contain", "registration") + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.get( + 'form[data-testid="registration-flow-code"] input[name="code"]', + ).type(code) + cy.submitCodeForm() + }) + + cy.get('[data-testid="ui/message/4040001"]').should( + "contain", + "The registration flow expired", + ) + + cy.updateConfigFile((config) => { + config.selfservice.methods.code.config.lifespan = "1h" + return config + }) + }) + + it("should show error message when required fields are missing", () => { + const email = gen.email() + + cy.get( + 'form[data-testid="registration-flow-code"] input[name="traits.email"]', + ).type(email) + cy.submitCodeForm() + + cy.url().should("contain", "registration") + + cy.removeAttribute( + ['form[data-testid="registration-flow-code"] input[name="code"]'], + "required", + ) + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property code is missing", + ) + + cy.get( + 'form[data-testid="registration-flow-code"] input[name="traits.email"]', + ).clear() + cy.get( + 'form[data-testid="registration-flow-code"] input[name="code"]', + ).type("invalid-code") + cy.removeAttribute( + [ + 'form[data-testid="registration-flow-code"] input[name="traits.email"]', + ], + "required", + ) + + cy.submitCodeForm() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property email is missing", + ) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts new file mode 100644 index 000000000000..6f7cd08c20b5 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -0,0 +1,279 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { should } from "chai" +import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration success with code method", () => { + ;[ + { + route: express.registration, + login: express.login, + app: "express" as "express", + profile: "code", + }, + // { + // route: react.registration, + // app: "react" as "react", + // profile: "code", + // }, + ].forEach(({ route, login, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.traits.schema.json", + ) + cy.setPostCodeRegistrationHooks([]) + cy.setupHooks("login", "after", "code", []) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should be able to sign up without session hook", () => { + const email = gen.email() + + cy.get( + "form[data-testid='registration-flow-code'] input[name='traits.email']", + ).type(email) + + cy.submitCodeForm() + + cy.url().should("contain", "registration") + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.get( + "form[data-testid='registration-flow-code'] input[name=code]", + ).type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.deleteMail({ atLeast: 1 }) + + cy.visit(login) + cy.get( + "form[data-testid='login-flow-code'] input[name=identifier]", + ).type(email) + cy.get("button[name=method][value=code]").click() + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get("form[data-testid='login-flow-code'] input[name=code]").type( + code, + ) + cy.get("button[name=method][value=code]").click() + }) + + cy.deleteMail({ atLeast: 1 }) + + if (app === "express") { + cy.get('a[href*="sessions"').click() + } + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to resend the registration code", async () => { + cy.setPostCodeRegistrationHooks([ + { + hook: "session", + }, + ]) + const email = gen.email() + + cy.get( + "form[data-testid='registration-flow-code'] input[name='traits.email']", + ).type(email) + + cy.submitCodeForm() + + cy.url().should("contain", "registration") + + cy.getRegistrationCodeFromEmail(email).then((code) => + cy.wrap(code).as("code1"), + ) + + cy.get( + "form[data-testid='registration-flow-code'] input[name='traits.email']", + ).should("have.value", email) + cy.get( + "form[data-testid='registration-flow-code'] input[name='method'][value='code'][type='hidden']", + ).should("exist") + cy.get( + "form[data-testid='registration-flow-code'] button[name='resend'][value='code']", + ).click() + + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code1").then((code1) => { + // previous code should not work + cy.get( + 'form[data-testid="registration-flow-code"] input[name="code"]', + ) + .clear() + .type(code1.toString()) + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4040003"]').should( + "contain.text", + "The registration code is invalid or has already been used. Please try again.", + ) + }) + + cy.get("@code2").then((code2) => { + cy.get( + 'form[data-testid="registration-flow-code"] input[name="code"]', + ) + .clear() + .type(code2.toString()) + cy.submitCodeForm() + }) + + if (app === "express") { + cy.get('a[href*="sessions"').click() + } + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should sign up and be logged in with session hook", () => { + cy.setPostCodeRegistrationHooks([ + { + hook: "session", + }, + ]) + + const email = gen.email() + + cy.get( + "form[data-testid='registration-flow-code'] input[name='traits.email']", + ).type(email) + + cy.submitCodeForm() + + cy.url().should("contain", "registration") + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.get( + "form[data-testid='registration-flow-code'] input[name=code]", + ).type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.deleteMail({ atLeast: 1 }) + + if (app === "express") { + cy.get('a[href*="sessions"').click() + } + cy.get("pre").should("contain.text", email) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to use multiple identifiers to signup with and sign in to", () => { + cy.setPostCodeRegistrationHooks([ + { + hook: "session", + }, + ]) + + // Setup complex schema + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.complex.traits.schema.json", + ) + + cy.visit(route) + + cy.get( + "form[data-testid='registration-flow-code'] input[name='traits.username']", + ).type(Math.random().toString(36)) + + const email = gen.email() + + cy.get( + "form[data-testid='registration-flow-code'] input[name='traits.email']", + ).type(email) + + const email2 = gen.email() + + cy.get( + "form[data-testid='registration-flow-code'] input[name='traits.email2']", + ).type(email2) + + cy.submitCodeForm() + + // intentionally use email 1 to verify the account + cy.url().should("contain", "registration") + cy.getRegistrationCodeFromEmail(email, { expectedCount: 2 }).then( + (code) => { + cy.get( + "form[data-testid='registration-flow-code'] input[name=code]", + ).type(code) + cy.get("button[name=method][value=code]").click() + }, + ) + + cy.deleteMail({ atLeast: 2 }) + + cy.logout() + + // Attempt to sign in with email 2 (should fail) + cy.visit(login) + cy.get( + "form[data-testid='login-flow-code'] input[name=identifier]", + ).type(email2) + + cy.get("button[name=method][value=code]").click() + + cy.getLoginCodeFromEmail(email2).then((code) => { + cy.get("form[data-testid='login-flow-code'] input[name=code]").type( + code, + ) + cy.get("button[name=method][value=code]").click() + }) + if (app === "express") { + cy.get('a[href*="sessions"').click() + } + + cy.getSession().should((session) => { + console.dir({ session }) + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(2) + expect( + identity.verifiable_addresses.filter((v) => v.value === email)[0] + .status, + ).to.equal("completed") + expect( + identity.verifiable_addresses.filter((v) => v.value === email2)[0] + .status, + ).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 91622e42dd2c..ef421bdf8736 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -221,6 +221,10 @@ Cypress.Commands.add("setPostPasswordRegistrationHooks", (hooks) => { cy.setupHooks("registration", "after", "password", hooks) }) +Cypress.Commands.add("setPostCodeRegistrationHooks", (hooks) => { + cy.setupHooks("registration", "after", "code", hooks) +}) + Cypress.Commands.add("shortLoginLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.login.lifespan = "100ms" @@ -377,6 +381,70 @@ Cypress.Commands.add( }, ) +Cypress.Commands.add( + "registerWithCode", + ({ email = gen.email(), code = undefined, query = {} } = {}) => { + console.log("Creating user account: ", { email }) + + cy.clearAllCookies() + + cy.request({ + url: APP_URL + "/self-service/registration/browser", + method: "GET", + followRedirect: false, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + qs: query || {}, + }).then(({ body, status }) => { + expect(status).to.eq(200) + const form = body.ui + return cy + .request({ + headers: { + Accept: "application/json", + }, + method: form.method, + body: mergeFields(form, { + method: "code", + "traits.email": email, + ...(code && { code }), + }), + url: form.action, + followRedirect: false, + }) + .then(({ body }) => { + if (!code) { + expect( + body.ui.nodes.find( + (f) => + f.group === "default" && f.attributes.name === "traits.email", + ).attributes.value, + ).to.eq(email) + return cy.getRegistrationCodeFromEmail(email).then((code) => { + return cy.request({ + headers: { + Accept: "application/json", + }, + method: form.method, + body: mergeFields(form, { + method: "code", + "traits.email": email, + code, + }), + url: form.action, + followRedirect: false, + }) + }) + } else { + expect(body.session).to.contain(email) + } + }) + }) + }, +) + Cypress.Commands.add( "registerApi", ({ email = gen.email(), password = gen.password(), fields = {} } = {}) => @@ -1171,30 +1239,44 @@ Cypress.Commands.add("expectSettingsSaved", () => { ) }) -Cypress.Commands.add("getMail", ({ removeMail = true } = {}) => { - let tries = 0 - const req = () => - cy.request(`${MAIL_API}/mail`).then((response) => { - expect(response.body).to.have.property("mailItems") - const count = response.body.mailItems.length - if (count === 0 && tries < 100) { - tries++ - cy.wait(pollInterval) - return req() - } - - expect(count).to.equal(1) - if (removeMail) { - return cy - .deleteMail({ atLeast: count }) - .then(() => Promise.resolve(response.body.mailItems[0])) - } +Cypress.Commands.add( + "getMail", + ({ removeMail = true, expectedCount = 1, email = undefined } = {}) => { + let tries = 0 + const req = () => + cy.request(`${MAIL_API}/mail`).then((response) => { + expect(response.body).to.have.property("mailItems") + const count = response.body.mailItems.length + if (count === 0 && tries < 100) { + tries++ + cy.wait(pollInterval) + return req() + } + let mailItem: any + if (email) { + mailItem = response.body.mailItems.find((m: any) => + m.toAddresses.includes(email), + ) + } else { + mailItem = response.body.mailItems[0] + } + console.log({ mailItems: response.body.mailItems }) + console.log({ mailItem }) + console.log({ email }) + + expect(count).to.equal(expectedCount) + if (removeMail) { + return cy.deleteMail({ atLeast: count }).then(() => { + return Promise.resolve(mailItem) + }) + } - return Promise.resolve(response.body.mailItems[0]) - }) + return Promise.resolve(mailItem) + }) - return req() -}) + return req() + }, +) Cypress.Commands.add("clearAllCookies", () => { cy.clearCookies({ domain: null }) @@ -1210,6 +1292,11 @@ Cypress.Commands.add("submitProfileForm", () => { cy.get('[name="method"][value="profile"]:disabled').should("not.exist") }) +Cypress.Commands.add("submitCodeForm", () => { + cy.get('button[name="method"][value="code"]').click() + cy.get('button[name="method"][value="code"]:disabled').should("not.exist") +}) + Cypress.Commands.add("clickWebAuthButton", (type: string) => { cy.get('*[data-testid="node/script/webauthn_script"]').should("exist") cy.wait(500) // Wait for script to load @@ -1375,3 +1462,40 @@ Cypress.Commands.add("getVerificationCodeFromEmail", (email) => { return code }) }) + +Cypress.Commands.add("enableRegistrationViaCode", (enable: boolean = true) => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code.registration_enabled = enable + return config + }) +}) + +Cypress.Commands.add("getRegistrationCodeFromEmail", (email, opts) => { + return cy + .getMail({ removeMail: true, email, ...opts }) + .should((message) => { + expect(message.subject).to.equal("Complete your account registration") + expect(message.toAddresses[0].trim()).to.equal(email) + }) + .then((message) => { + const code = extractRecoveryCode(message.body) + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + return code + }) +}) + +Cypress.Commands.add("getLoginCodeFromEmail", (email, opts) => { + return cy + .getMail({ removeMail: true, email, ...opts }) + .should((message) => { + expect(message.subject).to.equal("Login to your account") + expect(message.toAddresses[0].trim()).to.equal(email) + }) + .then((message) => { + const code = extractRecoveryCode(message.body) + expect(code).to.not.be.undefined + expect(code.length).to.equal(6) + return code + }) +}) diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/cypress/support/config.d.ts index e2bb5294b91e..762dbe4e090b 100644 --- a/test/e2e/cypress/support/config.d.ts +++ b/test/e2e/cypress/support/config.d.ts @@ -106,6 +106,8 @@ export type EnablesLinkMethod = boolean export type OverrideTheBaseURLWhichShouldBeUsedAsTheBaseForRecoveryAndVerificationLinks = string export type HowLongALinkIsValidFor = string +export type EnablesLoginWithCodeMethod = boolean +export type EnablesRegistrationWithCodeMethod = boolean export type EnablesCodeMethod = boolean export type HowLongACodeIsValidFor = string export type EnablesUsernameEmailAndPasswordMethod = boolean @@ -210,6 +212,7 @@ export type Provider = | "dingtalk" | "patreon" | "linkedin" + | "lark" export type OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons = string /** @@ -260,6 +263,20 @@ export type DataSourceName = string * You can override certain or all message templates by pointing this key to the path where the templates are located. */ export type OverrideMessageTemplates = string +/** + * Defines how emails will be sent, either through SMTP (default) or HTTP. + */ +export type DeliveryStrategy = "smtp" | "http" +/** + * This URL will be used to send the emails to. + */ +export type HTTPAddressOfAPIEndpoint = string +/** + * Define which auth mechanism to use for auth with the HTTP email provider + */ +export type AuthMechanisms = + | WebHookAuthApiKeyProperties + | WebHookAuthBasicAuthProperties /** * This URI will be used to connect to the SMTP server. Use the scheme smtps for implicit TLS sessions or smtp for explicit StartTLS/cleartext sessions. Please note that TLS is always enforced with certificate trust verification by default for security reasons on both schemes. With the smtp scheme you can use the query parameter (`?disable_starttls=true`) to allow cleartext sessions or (`?disable_starttls=false`) to enforce StartTLS (default behaviour). Additionally, use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates (default behaviour) on both implicit and explicit TLS sessions. */ @@ -291,17 +308,21 @@ export type SMSSenderAddress = string /** * This URL will be used to connect to the SMS provider. */ -export type HTTPAddressOfAPIEndpoint = string +export type HTTPAddressOfAPIEndpoint1 = string /** * Define which auth mechanism to use for auth with the SMS provider */ -export type AuthMechanisms = +export type AuthMechanisms1 = | WebHookAuthApiKeyProperties | WebHookAuthBasicAuthProperties /** * If set, the login and registration flows will handle the Ory OAuth 2.0 & OpenID `login_challenge` query parameter to serve as an OpenID Connect Provider. This URL should point to Ory Hydra when you are not running on the Ory Network and be left untouched otherwise. */ export type OAuth20ProviderURL = string +/** + * Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow. + */ +export type PersistOAuth2RequestBetweenFlows = boolean /** * Disable request logging for /health/alive and /health/ready endpoints */ @@ -506,6 +527,8 @@ export interface OryKratosConfiguration2 { config?: LinkConfiguration } code?: { + login_enabled?: EnablesLoginWithCodeMethod + registration_enabled?: EnablesRegistrationWithCodeMethod enabled?: EnablesCodeMethod config?: CodeConfiguration } @@ -674,15 +697,23 @@ export interface SelfServiceAfterRegistration { password?: SelfServiceAfterRegistrationMethod webauthn?: SelfServiceAfterRegistrationMethod oidc?: SelfServiceAfterRegistrationMethod + code?: SelfServiceAfterRegistrationMethod hooks?: SelfServiceHooks } export interface SelfServiceAfterRegistrationMethod { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault - hooks?: (SelfServiceSessionIssuerHook | SelfServiceWebHook)[] + hooks?: ( + | SelfServiceSessionIssuerHook + | SelfServiceWebHook + | SelfServiceShowVerificationUIHook + )[] } export interface SelfServiceSessionIssuerHook { hook: "session" } +export interface SelfServiceShowVerificationUIHook { + hook: "show_verification_ui" +} export interface SelfServiceBeforeLogin { hooks?: SelfServiceHooks } @@ -691,6 +722,7 @@ export interface SelfServiceAfterLogin { password?: SelfServiceAfterDefaultLoginMethod webauthn?: SelfServiceAfterDefaultLoginMethod oidc?: SelfServiceAfterOIDCLoginMethod + code?: SelfServiceAfterDefaultLoginMethod hooks?: ( | SelfServiceWebHook | SelfServiceSessionRevokerHook @@ -868,6 +900,8 @@ export interface CourierConfiguration { * Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned */ message_retries?: number + delivery_strategy?: DeliveryStrategy + http?: HTTPConfiguration smtp: SMTPConfiguration sms?: SMSSenderConfiguration } @@ -892,6 +926,61 @@ export interface EmailCourierTemplate { } subject?: string } +/** + * Configures outgoing emails using HTTP. + */ +export interface HTTPConfiguration { + request_config?: HttpRequestConfig +} +export interface HttpRequestConfig { + url?: HTTPAddressOfAPIEndpoint + /** + * The HTTP method to use (GET, POST, etc). Defaults to POST. + */ + method?: string + /** + * The HTTP headers that must be applied to request + */ + headers?: { + [k: string]: string | undefined + } + /** + * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads + */ + body?: string + auth?: AuthMechanisms + additionalProperties?: false +} +export interface WebHookAuthApiKeyProperties { + type: "api_key" + config: { + /** + * The name of the api key + */ + name: string + /** + * The value of the api key + */ + value: string + /** + * How the api key should be transferred + */ + in: "header" | "cookie" + } +} +export interface WebHookAuthBasicAuthProperties { + type: "basic_auth" + config: { + /** + * user name for basic auth + */ + user: string + /** + * password for basic auth + */ + password: string + } +} /** * Configures outgoing emails using the SMTP protocol. */ @@ -920,7 +1009,7 @@ export interface SMSSenderConfiguration { enabled?: boolean from?: SMSSenderAddress request_config?: { - url: HTTPAddressOfAPIEndpoint + url: HTTPAddressOfAPIEndpoint1 /** * The HTTP method to use (GET, POST, etc). */ @@ -935,44 +1024,14 @@ export interface SMSSenderConfiguration { * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads */ body?: string - auth?: AuthMechanisms + auth?: AuthMechanisms1 additionalProperties?: false } } -export interface WebHookAuthApiKeyProperties { - type: "api_key" - config: { - /** - * The name of the api key - */ - name: string - /** - * The value of the api key - */ - value: string - /** - * How the api key should be transferred - */ - in: "header" | "cookie" - } -} -export interface WebHookAuthBasicAuthProperties { - type: "basic_auth" - config: { - /** - * user name for basic auth - */ - user: string - /** - * password for basic auth - */ - password: string - } -} export interface OAuth2ProviderConfiguration { url?: OAuth20ProviderURL headers?: HTTPRequestHeaders - override_return_to?: boolean + override_return_to?: PersistOAuth2RequestBetweenFlows } /** * These headers will be passed in HTTP request to the OAuth2 Provider. diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index c9a52a0c7e8e..b6edfef4cd91 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -70,6 +70,17 @@ declare global { fields?: { [key: string]: any } }): Chainable> + /** + * Register a user with a code + * + * @param opts + */ + registerWithCode(opts: { + email: string + code?: string + query?: { [key: string]: string } + }): Chainable> + /** * Updates a user's settings using an API flow * @@ -89,7 +100,11 @@ declare global { * * @param opts */ - getMail(opts?: { removeMail: boolean }): Chainable + getMail(opts?: { + removeMail: boolean + expectedCount?: number + email?: string + }): Chainable performEmailVerification(opts?: { expect?: { email?: string; redirectTo?: string } @@ -166,7 +181,7 @@ declare global { | "verification" | "settings", phase: "before" | "after", - kind: "password" | "webauthn" | "oidc", + kind: "password" | "webauthn" | "oidc" | "code", hooks: Array<{ hook: string; config?: any }>, ): Chainable @@ -179,6 +194,15 @@ declare global { hooks: Array<{ hook: string; config?: any }>, ): Chainable + /** + * Sets the post code registration hook. + * + * @param hooks + */ + setPostCodeRegistrationHooks( + hooks: Array<{ hook: string; config?: any }>, + ): Chainable + /** * Submits a verification flow via the Browser * @@ -332,6 +356,11 @@ declare global { */ submitProfileForm(): Chainable + /** + * Submits a code form by clicking the button with method=code + */ + submitCodeForm(): Chainable + /** * Expect a CSRF error to occur * @@ -689,6 +718,28 @@ declare global { * Extracts a verification code from the received email */ getVerificationCodeFromEmail(email: string): Chainable + + /** + * Enables the registration code method + * @param enable + */ + enableRegistrationViaCode(enable: boolean): Chainable + + /** + * Extracts a registration code from the received email + */ + getRegistrationCodeFromEmail( + email: string, + opts?: { expectedCount: number }, + ): Chainable + + /** + * Extracts a login code from the received email + */ + getLoginCodeFromEmail( + email: string, + opts?: { expectedCount: number }, + ): Chainable } } } diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml new file mode 100644 index 000000000000..ed7365d83829 --- /dev/null +++ b/test/e2e/profiles/code/.kratos.yml @@ -0,0 +1,40 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + + login: + ui_url: http://localhost:4455/login + after: + code: + - hook: require_verified_address + error: + ui_url: http://localhost:4455/error + verification: + enabled: true + use: code + ui_url: http://localhost:4455/verification + recovery: + ui_url: http://localhost:4455/recovery + methods: + code: + registration_enabled: true + login_enabled: true + enabled: true + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/code/identity.traits.schema.json diff --git a/test/e2e/profiles/code/identity.code.only.traits.schema.json b/test/e2e/profiles/code/identity.code.only.traits.schema.json new file mode 100644 index 000000000000..97573e085d8c --- /dev/null +++ b/test/e2e/profiles/code/identity.code.only.traits.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + }, + "required": ["email"] + } + } +} diff --git a/test/e2e/profiles/code/identity.complex.traits.schema.json b/test/e2e/profiles/code/identity.complex.traits.schema.json new file mode 100644 index 000000000000..d6b5b817c510 --- /dev/null +++ b/test/e2e/profiles/code/identity.complex.traits.schema.json @@ -0,0 +1,69 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "username": { + "type": "string", + "title": "Your Username", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + }, + "email2": { + "type": "string", + "format": "email", + "title": "Your Second E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + } + }, + "required": ["email"] + } + } +} diff --git a/test/e2e/profiles/code/identity.traits.schema.json b/test/e2e/profiles/code/identity.traits.schema.json new file mode 100644 index 000000000000..55fccdc481e6 --- /dev/null +++ b/test/e2e/profiles/code/identity.traits.schema.json @@ -0,0 +1,34 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + }, + "verification": { + "via": "email" + } + } + } + }, + "required": ["email"] + } + } +} diff --git a/test/e2e/run.sh b/test/e2e/run.sh index fdb7b623b25f..ef17583b5b34 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -95,13 +95,13 @@ prepare() { export TEST_DATABASE_COCKROACHDB="cockroach://root@localhost:3446/defaultdb?sslmode=disable" fi - if [ -z ${NODE_UI_PATH+x} ]; then - node_ui_dir="$(mktemp -d -t ci-XXXXXXXXXX)/kratos-selfservice-ui-node" - git clone --depth 1 --branch master https://github.com/ory/kratos-selfservice-ui-node.git "$node_ui_dir" - (cd "$node_ui_dir" && npm i --legacy-peer-deps && npm run build) - else - node_ui_dir="${NODE_UI_PATH}" - fi + # if [ -z ${NODE_UI_PATH+x} ]; then + # node_ui_dir="$(mktemp -d -t ci-XXXXXXXXXX)/kratos-selfservice-ui-node" + # git clone --depth 1 --branch master https://github.com/ory/kratos-selfservice-ui-node.git "$node_ui_dir" + # (cd "$node_ui_dir" && npm i --legacy-peer-deps && npm run build) + # else + # node_ui_dir="${NODE_UI_PATH}" + # fi if [ -z ${RN_UI_PATH+x} ]; then rn_ui_dir="$(mktemp -d -t ci-XXXXXXXXXX)/kratos-selfservice-ui-react-native" @@ -136,7 +136,7 @@ prepare() { nc -zv localhost 4445 && exit 1 nc -zv localhost 4446 && exit 1 nc -zv localhost 4455 && exit 1 - nc -zv localhost 4456 && exit 1 + # nc -zv localhost 4456 && exit 1 nc -zv localhost 4457 && exit 1 nc -zv localhost 4458 && exit 1 nc -zv localhost 4744 && exit 1 @@ -218,19 +218,19 @@ prepare() { PORT=4746 HYDRA_ADMIN_URL=http://localhost:4745 ./hydra-kratos-login-consent >"${base}/test/e2e/hydra-kratos-ui.e2e.log" 2>&1 & ) - if [ -z ${NODE_UI_PATH+x} ]; then - ( - cd "$node_ui_dir" - PORT=4456 SECURITY_MODE=cookie npm run serve \ - >"${base}/test/e2e/ui-node.e2e.log" 2>&1 & - ) - else - ( - cd "$node_ui_dir" - PORT=4456 SECURITY_MODE=cookie npm run start \ - >"${base}/test/e2e/ui-node.e2e.log" 2>&1 & - ) - fi + # if [ -z ${NODE_UI_PATH+x} ]; then + # ( + # cd "$node_ui_dir" + # PORT=4456 SECURITY_MODE=cookie npm run serve \ + # >"${base}/test/e2e/ui-node.e2e.log" 2>&1 & + # ) + # else + # ( + # cd "$node_ui_dir" + # PORT=4456 SECURITY_MODE=cookie npm run start \ + # >"${base}/test/e2e/ui-node.e2e.log" 2>&1 & + # ) + # fi if [ -z ${REACT_UI_PATH+x} ]; then ( @@ -272,7 +272,7 @@ run() { nc -zv localhost 4433 && exit 1 ls -la . - for profile in email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do + for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do yq ea '. as $item ireduce ({}; . * $item )' test/e2e/profiles/kratos.base.yml "test/e2e/profiles/${profile}/.kratos.yml" > test/e2e/kratos.${profile}.yml cat "test/e2e/kratos.${profile}.yml" | envsubst | sponge "test/e2e/kratos.${profile}.yml" done diff --git a/text/id.go b/text/id.go index f216074e191f..dd002059df77 100644 --- a/text/id.go +++ b/text/id.go @@ -23,6 +23,8 @@ const ( InfoSelfServiceLoginContinueWebAuthn // 1010011 InfoSelfServiceLoginWebAuthnPasswordless // 1010012 InfoSelfServiceLoginContinue // 1010013 + InfoSelfServiceLoginEmailWithCodeSent // 1010014 + InfoSelfServiceLoginCode // 1010015 ) const ( @@ -34,11 +36,13 @@ const ( ) const ( - InfoSelfServiceRegistrationRoot ID = 1040000 + iota // 1040000 - InfoSelfServiceRegistration // 1040001 - InfoSelfServiceRegistrationWith // 1040002 - InfoSelfServiceRegistrationContinue // 1040003 - InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 + InfoSelfServiceRegistrationRoot ID = 1040000 + iota // 1040000 + InfoSelfServiceRegistration // 1040001 + InfoSelfServiceRegistrationWith // 1040002 + InfoSelfServiceRegistrationContinue // 1040003 + InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 + InfoSelfServiceRegistrationEmailWithCodeSent // 1040005 + InfoSelfServiceRegistrationRegisterCode // 1040006 ) const ( @@ -83,6 +87,8 @@ const ( InfoNodeLabelContinue // 1070009 InfoNodeLabelRecoveryCode // 1070010 InfoNodeLabelVerificationCode // 1070011 + InfoNodeLabelRegistrationCode // 1070012 + InfoNodeLabelLoginCode // 1070013 ) const ( @@ -122,21 +128,27 @@ const ( ErrorValidationWrongType ErrorValidationDuplicateCredentialsOnOIDCLink ErrorValidationDuplicateCredentialsWithHints + ErrorValidationNoCodeUser + ErrorValidationTraitsMismatch ) const ( - ErrorValidationLogin ID = 4010000 + iota // 4010000 - ErrorValidationLoginFlowExpired // 4010001 - ErrorValidationLoginNoStrategyFound // 4010002 - ErrorValidationRegistrationNoStrategyFound // 4010003 - ErrorValidationSettingsNoStrategyFound // 4010004 - ErrorValidationRecoveryNoStrategyFound // 4010005 - ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLogin ID = 4010000 + iota // 4010000 + ErrorValidationLoginFlowExpired // 4010001 + ErrorValidationLoginNoStrategyFound // 4010002 + ErrorValidationRegistrationNoStrategyFound // 4010003 + ErrorValidationSettingsNoStrategyFound // 4010004 + ErrorValidationRecoveryNoStrategyFound // 4010005 + ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLoginRetrySuccess // 4010007 + ErrorValidationLoginCodeInvalidOrAlreadyUsed // 4010008 ) const ( - ErrorValidationRegistration ID = 4040000 + iota - ErrorValidationRegistrationFlowExpired + ErrorValidationRegistration ID = 4040000 + iota + ErrorValidationRegistrationFlowExpired // 4040001 + ErrorValidateionRegistrationRetrySuccess // 4040002 + ErrorValidationRegistrationCodeInvalidOrAlreadyUsed // 4040003 ) const ( diff --git a/text/message_login.go b/text/message_login.go index 0a751530ddf0..f062034954a9 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -183,3 +183,38 @@ func NewInfoSelfServiceLoginContinue() *Message { Type: Info, } } + +func NewLoginEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceLoginEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the login.", + Context: context(nil), + } +} + +func NewErrorValidationLoginCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationLoginCodeInvalidOrAlreadyUsed, + Text: "The login code is invalid or has already been used. Please try again.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationLoginRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidationLoginRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + Context: context(nil), + } +} + +func NewInfoSelfServiceLoginCode() *Message { + return &Message{ + ID: InfoSelfServiceLoginCode, + Type: Info, + Text: "Sign in with code", + } +} diff --git a/text/message_node.go b/text/message_node.go index f3712ea75b6d..d9f3a03a0009 100644 --- a/text/message_node.go +++ b/text/message_node.go @@ -27,6 +27,22 @@ func NewInfoNodeLabelRecoveryCode() *Message { } } +func NewInfoNodeLabelRegistrationCode() *Message { + return &Message{ + ID: InfoNodeLabelRegistrationCode, + Text: "Registration code", + Type: Info, + } +} + +func NewInfoNodeLabelLoginCode() *Message { + return &Message{ + ID: InfoNodeLabelLoginCode, + Text: "Login code", + Type: Info, + } +} + func NewInfoNodeInputPassword() *Message { return &Message{ ID: InfoNodeLabelInputPassword, diff --git a/text/message_registration.go b/text/message_registration.go index be9135cd06bb..51a22b27bfd5 100644 --- a/text/message_registration.go +++ b/text/message_registration.go @@ -54,3 +54,38 @@ func NewInfoSelfServiceRegistrationRegisterWebAuthn() *Message { Type: Info, } } + +func NewRegistrationEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the registration.", + Context: context(nil), + } +} + +func NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationRegistrationCodeInvalidOrAlreadyUsed, + Text: "The registration code is invalid or has already been used. Please try again.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationRegistrationRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidateionRegistrationRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + Context: context(nil), + } +} + +func NewInfoSelfServiceRegistrationRegisterCode() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterCode, + Text: "Sign up with code", + Type: Info, + } +} diff --git a/text/message_validation.go b/text/message_validation.go index dc62bf6157f2..c080f02499b6 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -264,3 +264,21 @@ func NewErrorValidationSuchNoWebAuthnUser() *Message { Context: context(nil), } } + +func NewErrorValidationNoCodeUser() *Message { + return &Message{ + ID: ErrorValidationNoCodeUser, + Text: "This account does not exist or has not setup sign in with code.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationTraitsMismatch() *Message { + return &Message{ + ID: ErrorValidationTraitsMismatch, + Text: "The provided traits do not match the traits previously associated with this flow.", + Type: Error, + Context: context(nil), + } +} diff --git a/ui/container/container.go b/ui/container/container.go index af74fa0f00ce..b0e33714de47 100644 --- a/ui/container/container.go +++ b/ui/container/container.go @@ -190,7 +190,7 @@ func (c *Container) ParseError(group node.UiNodeGroup, err error) error { default: // The pointer can be ignored because if there is an error, we'll just use // the empty field (global error). - var causes = e.Causes + causes := e.Causes if len(e.Causes) == 0 { pointer, _ := jsonschemax.JSONPointerToDotNotation(e.InstancePtr) c.AddMessage(group, translateValidationError(e), pointer) @@ -310,6 +310,7 @@ func (c *Container) AddMessage(group node.UiNodeGroup, err *text.Message, setFor func (c *Container) Scan(value interface{}) error { return sqlxx.JSONScan(c, value) } + func (c *Container) Value() (driver.Value, error) { return sqlxx.JSONValue(c) } diff --git a/x/xsql/sql.go b/x/xsql/sql.go index f2354d082ca1..7ee2591bcd74 100644 --- a/x/xsql/sql.go +++ b/x/xsql/sql.go @@ -28,6 +28,8 @@ import ( func CleanSQL(t testing.TB, c *pop.Connection) { ctx := context.Background() for _, table := range []string{ + new(code.LoginCode).TableName(ctx), + new(code.RegistrationCode).TableName(ctx), new(continuity.Container).TableName(ctx), new(courier.MessageDispatch).TableName(), new(courier.Message).TableName(ctx),