From b82207388ac90e1d4f802014a482928128b0504a Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Fri, 30 Jun 2023 18:29:10 +0200 Subject: [PATCH 01/24] feat: login with code --- identity/credentials.go | 4 + identity/credentials_code.go | 9 ++ selfservice/flow/recovery/flow.go | 2 +- selfservice/flow/recovery/handler.go | 4 +- selfservice/flow/recovery/strategy.go | 2 +- selfservice/flow/verification/flow.go | 2 +- selfservice/flow/verification/handler.go | 4 +- selfservice/flow/verification/strategy.go | 2 +- .../strategy/code/.schema/login.schema.json | 27 ++++ selfservice/strategy/code/code_login.go | 22 ++++ selfservice/strategy/code/schema.go | 3 + selfservice/strategy/code/strategy.go | 17 ++- selfservice/strategy/code/strategy_login.go | 119 ++++++++++++++++++ .../strategy/code/strategy_recovery.go | 6 +- .../strategy/code/strategy_verification.go | 2 +- 15 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 identity/credentials_code.go create mode 100644 selfservice/strategy/code/.schema/login.schema.json create mode 100644 selfservice/strategy/code/code_login.go create mode 100644 selfservice/strategy/code/strategy_login.go diff --git a/identity/credentials.go b/identity/credentials.go index af79ba3f2ed6..4efea49611f7 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -94,6 +94,8 @@ func (c CredentialsType) ToUiNodeGroup() node.UiNodeGroup { return node.WebAuthnGroup case CredentialsTypeLookup: return node.LookupGroup + case CredentialsTypeCodeAuth: + return node.CodeGroup default: return node.DefaultGroup } @@ -106,6 +108,7 @@ const ( CredentialsTypeTOTP CredentialsType = "totp" CredentialsTypeLookup CredentialsType = "lookup_secret" CredentialsTypeWebAuthn CredentialsType = "webauthn" + CredentialsTypeCodeAuth CredentialsType = "code" ) var AllCredentialTypes = []CredentialsType{ @@ -131,6 +134,7 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { CredentialsTypeTOTP, CredentialsTypeLookup, CredentialsTypeWebAuthn, + CredentialsTypeCodeAuth, CredentialsTypeRecoveryLink, CredentialsTypeRecoveryCode, } { diff --git a/identity/credentials_code.go b/identity/credentials_code.go new file mode 100644 index 000000000000..b66d0964bbd9 --- /dev/null +++ b/identity/credentials_code.go @@ -0,0 +1,9 @@ +package identity + +// CredentialsOTP represents an OTP code +// +// swagger:model identityCredentialsOTP +type CredentialsOTP struct { + // CodeHMAC represents the HMACed value of the login/registration code + CodeHMAC string `json:"code_hmac"` +} diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go index 42770783fb42..c6cfc3dfa4c9 100644 --- a/selfservice/flow/recovery/flow.go +++ b/selfservice/flow/recovery/flow.go @@ -133,7 +133,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques } if strategy != nil { - flow.Active = sqlxx.NullString(strategy.RecoveryNodeGroup()) + flow.Active = sqlxx.NullString(strategy.NodeGroup()) if err := strategy.PopulateRecoveryMethod(r, flow); err != nil { return nil, err } diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go index 24aed7695bba..90a62aab829c 100644 --- a/selfservice/flow/recovery/handler.go +++ b/selfservice/flow/recovery/handler.go @@ -430,12 +430,12 @@ func (h *Handler) updateRecoveryFlow(w http.ResponseWriter, r *http.Request, ps } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { - h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, ss.RecoveryNodeGroup(), err) + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } found = true - g = ss.RecoveryNodeGroup() + g = ss.NodeGroup() break } diff --git a/selfservice/flow/recovery/strategy.go b/selfservice/flow/recovery/strategy.go index 28e28673f1d3..3594420167df 100644 --- a/selfservice/flow/recovery/strategy.go +++ b/selfservice/flow/recovery/strategy.go @@ -26,7 +26,7 @@ const ( type ( Strategy interface { RecoveryStrategyID() string - RecoveryNodeGroup() node.UiNodeGroup + NodeGroup() node.UiNodeGroup PopulateRecoveryMethod(*http.Request, *Flow) error Recover(w http.ResponseWriter, r *http.Request, f *Flow) (err error) } diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go index 71a484f7d9c5..e41509e19c8e 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -149,7 +149,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques } if strategy != nil { - f.Active = sqlxx.NullString(strategy.VerificationNodeGroup()) + f.Active = sqlxx.NullString(strategy.NodeGroup()) if err := strategy.PopulateVerificationMethod(r, f); err != nil { return nil, err } diff --git a/selfservice/flow/verification/handler.go b/selfservice/flow/verification/handler.go index 977cb7bd8616..83a21418e83b 100644 --- a/selfservice/flow/verification/handler.go +++ b/selfservice/flow/verification/handler.go @@ -427,12 +427,12 @@ func (h *Handler) updateVerificationFlow(w http.ResponseWriter, r *http.Request, } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { - h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.VerificationNodeGroup(), err) + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } found = true - g = ss.VerificationNodeGroup() + g = ss.NodeGroup() break } diff --git a/selfservice/flow/verification/strategy.go b/selfservice/flow/verification/strategy.go index 9cb4ff848920..3d270bfb8732 100644 --- a/selfservice/flow/verification/strategy.go +++ b/selfservice/flow/verification/strategy.go @@ -26,7 +26,7 @@ const ( type ( Strategy interface { VerificationStrategyID() string - VerificationNodeGroup() node.UiNodeGroup + NodeGroup() node.UiNodeGroup PopulateVerificationMethod(*http.Request, *Flow) error Verify(w http.ResponseWriter, r *http.Request, f *Flow) (err error) SendVerificationEmail(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress) error diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json new file mode 100644 index 000000000000..6572ba9f9d89 --- /dev/null +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/code/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "code" + ] + }, + "code": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "csrf_token": { + "type": "string" + } + } +} diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go new file mode 100644 index 000000000000..9243f1015e26 --- /dev/null +++ b/selfservice/strategy/code/code_login.go @@ -0,0 +1,22 @@ +package code + +import ( + "database/sql" + + "github.com/gofrs/uuid" +) + +type LoginRegistrationCode struct { + // ID is the primary key + // + // 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"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` +} diff --git a/selfservice/strategy/code/schema.go b/selfservice/strategy/code/schema.go index 69d5bd07393e..24674c9a476d 100644 --- a/selfservice/strategy/code/schema.go +++ b/selfservice/strategy/code/schema.go @@ -12,3 +12,6 @@ var recoveryMethodSchema []byte //go:embed .schema/verification.schema.json var verificationMethodSchema []byte + +//go:embed .schema/login.schema.json +var loginMethodSchema []byte diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index b35d8bda30ff..262cc40e8963 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -9,7 +9,9 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/errorx" + "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/session" @@ -28,6 +30,9 @@ var _ verification.Strategy = new(Strategy) var _ verification.AdminHandler = new(Strategy) var _ verification.PublicHandler = new(Strategy) +var _ login.Strategy = new(Strategy) +var _ registration.Strategy = new(Strategy) + type ( // FlowMethod contains the configuration for this selfservice strategy. FlowMethod struct { @@ -65,6 +70,12 @@ type ( verification.StrategyProvider verification.HookExecutorProvider + login.StrategyProvider + login.HookExecutorProvider + login.FlowPersistenceProvider + + registration.StrategyProvider + RecoveryCodePersistenceProvider VerificationCodePersistenceProvider SenderProvider @@ -82,11 +93,7 @@ func NewStrategy(deps strategyDependencies) *Strategy { return &Strategy{deps: deps, dx: decoderx.NewHTTP()} } -func (s *Strategy) RecoveryNodeGroup() node.UiNodeGroup { - return node.CodeGroup -} - -func (s *Strategy) VerificationNodeGroup() node.UiNodeGroup { +func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.CodeGroup } diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go new file mode 100644 index 000000000000..126fea60d8b2 --- /dev/null +++ b/selfservice/strategy/code/strategy_login.go @@ -0,0 +1,119 @@ +package code + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "github.com/gofrs/uuid" + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "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"` +} + +func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) { +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeCodeAuth +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: identity.CredentialsTypeCodeAuth, + AAL: identity.AuthenticatorAssuranceLevel2, + } +} + +func (s *Strategy) HandleLoginError(w http.ResponseWriter, r *http.Request, flow *login.Flow, body *loginSubmitPayload, err error) error { + if flow != nil { + email := "" + if body != nil { + email = body.Identifier + } + + flow.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + flow.UI.GetNodes().Upsert( + node.NewInputField("identifier", email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + } + + return err +} + +func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, lf *login.Flow) error { + if lf.Type != flow.TypeBrowser { + return nil + } + + if requestedAAL == identity.AuthenticatorAssuranceLevel2 { + return nil + } + + 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 +} + +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 { + return nil, err + } + + if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.deps); err != nil { + return nil, err + } + + var p loginSubmitPayload + if err := s.dx.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginMethodSchema), + decoderx.HTTPDecoderAllowedMethods("POST"), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.HandleLoginError(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 nil, s.HandleLoginError(w, r, f, &p, err) + } + + i, c, err := s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), p.Identifier) + + if err != nil { + return nil, s.HandleLoginError(w, r, f, &p, 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) + } + + f.Active = identity.CredentialsTypeCodeAuth + if err = s.deps.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + return nil, s.HandleLoginError(w, r, f, &p, err) + } + + return i, nil +} diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index d06c5e2c9009..92ac3b66d2a5 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -281,7 +281,7 @@ func (s Strategy) isCodeFlow(f *recovery.Flow) bool { if err != nil { return false } - return value == s.RecoveryNodeGroup().String() + return value == s.NodeGroup().String() } func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.Flow) (err error) { @@ -539,13 +539,13 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - f.Active = sqlxx.NullString(s.RecoveryNodeGroup()) + f.Active = sqlxx.NullString(s.NodeGroup()) f.State = recovery.StateEmailSent f.UI.Messages.Set(text.NewRecoveryEmailWithCodeSent()) f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) - f.UI.Nodes.Append(node.NewInputField("method", s.RecoveryNodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) f.UI. GetNodes(). diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index 9a0902a404bb..4de4bf9d16a5 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -48,7 +48,7 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F ) // Required for the re-send code button nodes.Append( - node.NewInputField("method", s.VerificationNodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), + node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), ) f.UI.Messages.Set(text.NewVerificationEmailWithCodeSent()) default: From b6196ff59adf663969adb1b2c5484fcdaf3cfdb8 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 02/24] 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 d3775d1eafb1..afdad6fcd3fe 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -27,8 +27,10 @@ import ( "github.com/ory/x/clidoc" ) -var aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) -var inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) +var ( + aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) + inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) +) var messages map[string]*text.Message @@ -151,6 +153,18 @@ func init() { "NewInfoSelfServiceContinueLoginWebAuthn": text.NewInfoSelfServiceContinueLoginWebAuthn(), "NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(), "NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(), + "NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(), + "NewLoginEmailWithCodeSent": text.NewLoginEmailWithCodeSent(), + "NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed": text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed(), + "NewErrorValidationLoginCodeInvalidOrAlreadyUsed": text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed(), + "NewErrorValidationNoCodeUser": text.NewErrorValidationNoCodeUser(), + "NewInfoNodeLabelRegistrationCode": text.NewInfoNodeLabelRegistrationCode(), + "NewInfoNodeLabelLoginCode": text.NewInfoNodeLabelLoginCode(), + "NewErrorValidationLoginRetrySuccessful": text.NewErrorValidationLoginRetrySuccessful(), + "NewErrorValidationTraitsMismatch": text.NewErrorValidationTraitsMismatch(), + "NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(), + "NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(), + "NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(), } } diff --git a/courier/email_templates.go b/courier/email_templates.go index 8d5d51f0ceaa..d2bae0a197e4 100644 --- a/courier/email_templates.go +++ b/courier/email_templates.go @@ -40,6 +40,8 @@ const ( TypeVerificationCodeValid TemplateType = "verification_code_valid" TypeOTP TemplateType = "otp" TypeTestStub TemplateType = "stub" + TypeLoginCodeValid TemplateType = "login_code_valid" + TypeRegistrationCodeValid TemplateType = "registration_code_valid" ) func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) { @@ -60,6 +62,10 @@ func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) { return TypeVerificationCodeInvalid, nil case *email.VerificationCodeValid: return TypeVerificationCodeValid, nil + case *email.LoginCodeValid: + return TypeLoginCodeValid, nil + case *email.RegistrationCodeValid: + return TypeRegistrationCodeValid, nil case *email.TestStub: return TypeTestStub, nil default: @@ -123,6 +129,18 @@ func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTem return nil, err } return email.NewTestStub(d, &t), nil + case TypeLoginCodeValid: + var t email.LoginCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewLoginCodeValid(d, &t), nil + case TypeRegistrationCodeValid: + var t email.RegistrationCodeValidModel + if err := json.Unmarshal(msg.TemplateData, &t); err != nil { + return nil, err + } + return email.NewRegistrationCodeValid(d, &t), nil default: return nil, errors.Errorf("received unexpected message template type: %s", msg.TemplateType) } diff --git a/courier/email_templates_test.go b/courier/email_templates_test.go index 2e8f8520bb7f..40afb5dc6863 100644 --- a/courier/email_templates_test.go +++ b/courier/email_templates_test.go @@ -27,6 +27,8 @@ func TestGetTemplateType(t *testing.T) { courier.TypeVerificationCodeInvalid: &email.VerificationCodeInvalid{}, courier.TypeVerificationCodeValid: &email.VerificationCodeValid{}, courier.TypeTestStub: &email.TestStub{}, + courier.TypeLoginCodeValid: &email.LoginCodeValid{}, + courier.TypeRegistrationCodeValid: &email.RegistrationCodeValid{}, } { t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) { actualType, err := courier.GetEmailTemplateType(tmpl) @@ -50,6 +52,8 @@ func TestNewEmailTemplateFromMessage(t *testing.T) { courier.TypeVerificationCodeInvalid: email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{To: "baz"}), courier.TypeVerificationCodeValid: email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{To: "faz", VerificationURL: "http://bar.foo", VerificationCode: "123456678"}), courier.TypeTestStub: email.NewTestStub(reg, &email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), + courier.TypeLoginCodeValid: email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{To: "far", LoginCode: "123456"}), + courier.TypeRegistrationCodeValid: email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{To: "far", RegistrationCode: "123456"}), } { t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { tmplData, err := json.Marshal(expectedTmpl) @@ -84,7 +88,6 @@ func TestNewEmailTemplateFromMessage(t *testing.T) { actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext(ctx) require.NoError(t, err) require.Equal(t, expectedBodyPlaintext, actualBodyPlaintext) - }) } } diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl new file mode 100644 index 000000000000..505684b9849b --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please login to your account by entering the following code: + +{{ .LoginCode }} diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 000000000000..505684b9849b --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please login to your account by entering the following code: + +{{ .LoginCode }} diff --git a/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl new file mode 100644 index 000000000000..19d7bfd57d49 --- /dev/null +++ b/courier/template/courier/builtin/templates/login_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Login to your account diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl new file mode 100644 index 000000000000..6b9c31799995 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please complete your account registration by entering the following code: + +{{ .RegistrationCode }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl new file mode 100644 index 000000000000..6b9c31799995 --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.body.plaintext.gotmpl @@ -0,0 +1,5 @@ +Hi, + +please complete your account registration by entering the following code: + +{{ .RegistrationCode }} diff --git a/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl new file mode 100644 index 000000000000..0f36292619ef --- /dev/null +++ b/courier/template/courier/builtin/templates/registration_code/valid/email.subject.gotmpl @@ -0,0 +1 @@ +Complete your account registration diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go new file mode 100644 index 000000000000..2debc3a0cb7c --- /dev/null +++ b/courier/template/email/login_code_valid.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + LoginCodeValid struct { + deps template.Dependencies + model *LoginCodeValidModel + } + LoginCodeValidModel struct { + To string + LoginCode string + Identity map[string]interface{} + } +) + +func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid { + return &LoginCodeValid{deps: d, model: m} +} + +func (t *LoginCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *LoginCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.subject.gotmpl", "login_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *LoginCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.gotmpl", "login_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.HTML) +} + +func (t *LoginCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.plaintext.gotmpl", "login_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.PlainText) +} + +func (t *LoginCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} diff --git a/courier/template/email/login_code_valid_test.go b/courier/template/email/login_code_valid_test.go new file mode 100644 index 000000000000..dca97defe08c --- /dev/null +++ b/courier/template/email/login_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/internal" +) + +func TestLoginCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/login_code/valid", courier.TypeLoginCodeValid) + }) +} diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go new file mode 100644 index 000000000000..f7e39e334976 --- /dev/null +++ b/courier/template/email/registration_code_valid.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/ory/kratos/courier/template" +) + +type ( + RegistrationCodeValid struct { + deps template.Dependencies + model *RegistrationCodeValidModel + } + RegistrationCodeValidModel struct { + To string + Traits map[string]interface{} + RegistrationCode string + } +) + +func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid { + return &RegistrationCodeValid{deps: d, model: m} +} + +func (t *RegistrationCodeValid) EmailRecipient() (string, error) { + return t.model.To, nil +} + +func (t *RegistrationCodeValid) EmailSubject(ctx context.Context) (string, error) { + subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.subject.gotmpl", "registration_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Subject) + + return strings.TrimSpace(subject), err +} + +func (t *RegistrationCodeValid) EmailBody(ctx context.Context) (string, error) { + return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.gotmpl", "registration_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.HTML) +} + +func (t *RegistrationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.plaintext.gotmpl", "registration_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.PlainText) +} + +func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} diff --git a/courier/template/email/registration_code_valid_test.go b/courier/template/email/registration_code_valid_test.go new file mode 100644 index 000000000000..be4cfe8059ea --- /dev/null +++ b/courier/template/email/registration_code_valid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package email_test + +import ( + "context" + "testing" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/testhelpers" + "github.com/ory/kratos/internal" +) + +func TestRegistrationCodeValid(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{}) + + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/registration_code/valid", courier.TypeRegistrationCodeValid) + }) +} diff --git a/courier/template/template.go b/courier/template/template.go index 3ee99428aa5b..483c40bd2f5e 100644 --- a/courier/template/template.go +++ b/courier/template/template.go @@ -19,6 +19,8 @@ type ( CourierTemplatesVerificationValid() *config.CourierEmailTemplate CourierTemplatesRecoveryInvalid() *config.CourierEmailTemplate CourierTemplatesRecoveryValid() *config.CourierEmailTemplate + CourierTemplatesLoginValid() *config.CourierEmailTemplate + CourierTemplatesRegistrationValid() *config.CourierEmailTemplate } Dependencies interface { diff --git a/courier/template/testhelpers/testhelpers.go b/courier/template/testhelpers/testhelpers.go index 936eedb0a65e..6c2923dbe416 100644 --- a/courier/template/testhelpers/testhelpers.go +++ b/courier/template/testhelpers/testhelpers.go @@ -40,7 +40,8 @@ func SetupRemoteConfig(t *testing.T, ctx context.Context, plaintext string, html func TestRendered(t *testing.T, ctx context.Context, tpl interface { EmailBody(context.Context) (string, error) EmailSubject(context.Context) (string, error) -}) { +}, +) { rendered, err := tpl.EmailBody(ctx) require.NoError(t, err) assert.NotEmpty(t, rendered) @@ -83,6 +84,10 @@ func TestRemoteTemplates(t *testing.T, basePath string, tmplType courier.Templat return email.NewVerificationCodeInvalid(d, &email.VerificationCodeInvalidModel{}) case courier.TypeVerificationCodeValid: return email.NewVerificationCodeValid(d, &email.VerificationCodeValidModel{}) + case courier.TypeLoginCodeValid: + return email.NewLoginCodeValid(d, &email.LoginCodeValidModel{}) + case courier.TypeRegistrationCodeValid: + return email.NewRegistrationCodeValid(d, &email.RegistrationCodeValidModel{}) default: return nil } diff --git a/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 365b19323fd7..feb115c00f7c 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 d0ffa2d2c198..b69c39b3bd22 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -97,11 +97,12 @@ type RegistryDefault struct { persister persistence.Persister migrationStatus popx.MigrationStatuses - hookVerifier *hook.Verifier - hookSessionIssuer *hook.SessionIssuer - hookSessionDestroyer *hook.SessionDestroyer - hookAddressVerifier *hook.AddressVerifier - hookShowVerificationUI *hook.ShowVerificationUIHook + hookVerifier *hook.Verifier + hookSessionIssuer *hook.SessionIssuer + hookSessionDestroyer *hook.SessionDestroyer + hookAddressVerifier *hook.AddressVerifier + hookShowVerificationUI *hook.ShowVerificationUIHook + hookCodeAddressVerifier *hook.CodeAddressVerifier identityHandler *identity.Handler identityValidator *identity.Validator @@ -327,10 +328,28 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} { return m.selfserviceStrategies } +func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool { + switch id { + case identity.CredentialsTypeCodeAuth.String(): + return m.Config().SelfServiceCodeStrategy(ctx).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) } } @@ -352,7 +371,7 @@ func (m *RegistryDefault) AllRegistrationStrategies() registration.Strategies { func (m *RegistryDefault) LoginStrategies(ctx context.Context) (loginStrategies login.Strategies) { for _, strategy := range m.selfServiceStrategies() { if s, ok := strategy.(login.Strategy); ok { - if m.Config().SelfServiceStrategy(ctx, string(s.ID())).Enabled { + if m.strategyLoginEnabled(ctx, s.ID().String()) { loginStrategies = append(loginStrategies, s) } } @@ -660,7 +679,6 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize m.persister = p.WithNetworkID(net.ID) return nil }, bc) - if err != nil { return err } @@ -744,6 +762,14 @@ func (m *RegistryDefault) VerificationCodePersister() code.VerificationCodePersi return m.Persister() } +func (m *RegistryDefault) RegistrationCodePersister() code.RegistrationCodePersister { + return m.Persister() +} + +func (m *RegistryDefault) LoginCodePersister() code.LoginCodePersister { + return m.Persister() +} + func (m *RegistryDefault) Persister() persistence.Persister { return m.persister } diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index 6efffc05a777..c3f809d2144e 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -15,6 +15,13 @@ func (m *RegistryDefault) HookVerifier() *hook.Verifier { return m.hookVerifier } +func (m *RegistryDefault) HookCodeAddressVerifier() *hook.CodeAddressVerifier { + if m.hookCodeAddressVerifier == nil { + m.hookCodeAddressVerifier = hook.NewCodeAddressVerifier(m) + } + return m.hookCodeAddressVerifier +} + func (m *RegistryDefault) HookSessionIssuer() *hook.SessionIssuer { if m.hookSessionIssuer == nil { m.hookSessionIssuer = hook.NewSessionIssuer(m) diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 7f78517891f0..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 73d144e0be9e..2c587e53dcd1 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 c581b117ac04..be486e3723ae 100644 --- a/selfservice/flow/flow.go +++ b/selfservice/flow/flow.go @@ -38,6 +38,9 @@ type Flow interface { GetRequestURL() string AppendTo(*url.URL) *url.URL GetUI() *container.Container + GetState() State + SetState(State) + GetFlowName() FlowName } type FlowWithRedirect interface { diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index a0276175e2e4..e46698290819 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -124,8 +124,19 @@ type Flow struct { // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", // and only on creating the login flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // State represents the state of this request: + // + // - choose_method: ask the user to choose a method to sign in with + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the login challenge was passed. + // + // required: true + State State `json:"state" faker:"-" db:"state"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, flowType flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -164,6 +175,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques r.URL.Query().Get("aal"), string(identity.AuthenticatorAssuranceLevel1)))), InternalContext: []byte("{}"), + State: flow.StateChooseMethod, }, nil } @@ -251,3 +263,15 @@ func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (o x.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowLoginReturnTo(ctx, f.Active.String())), } } + +func (f *Flow) GetState() flow.State { + return flow.State(f.State) +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.LoginFlow +} + +func (f *Flow) SetState(state flow.State) { + f.State = State(state) +} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index c04b8cd0b2e3..203a62fdf989 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -142,7 +142,6 @@ func (e *HookExecutor) PostLoginHook( x.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())), x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(r.Context(), a.Active.String())), ) - if err != nil { return err } diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go index 74160b8d2566..9f1a144ffc2d 100644 --- a/selfservice/flow/login/sort.go +++ b/selfservice/flow/login/sort.go @@ -15,6 +15,7 @@ func sortNodes(ctx context.Context, n node.Nodes) error { node.OpenIDConnectGroup, node.DefaultGroup, node.WebAuthnGroup, + node.CodeGroup, node.PasswordGroup, node.TOTPGroup, node.LookupGroup, diff --git a/selfservice/flow/login/state.go b/selfservice/flow/login/state.go new file mode 100644 index 000000000000..576fad6d9f05 --- /dev/null +++ b/selfservice/flow/login/state.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import "github.com/ory/kratos/selfservice/flow" + +// Login Flow State +// +// The state represents the state of the login flow. +// +// - choose_method: ask the user to choose a method (e.g. login account via email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the login challenge was passed. +// +// swagger:model loginFlowState +type State = flow.State diff --git a/selfservice/flow/name.go b/selfservice/flow/name.go new file mode 100644 index 000000000000..1b766c6662f6 --- /dev/null +++ b/selfservice/flow/name.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +// FlowName is the flow name. +// +// The flow name can be one of: +// - 'login' +// - 'registration' +// - 'settings' +// - 'recovery' +// - 'verification' +// +// swagger:ignore +type FlowName string + +const ( + LoginFlow FlowName = "login" + RegistrationFlow FlowName = "registration" + SettingsFlow FlowName = "settings" + RecoveryFlow FlowName = "recovery" + VerificationFlow FlowName = "verification" +) + +func (t Type) String() string { + return string(t) +} diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go index 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 b16bf2048ce5..39843a9e5b5c 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -115,8 +115,18 @@ type Flow struct { // This is only set if the client has requested a session token exchange code, and if the flow is of type "api", // and only on creating the flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + + // State represents the state of this request: + // + // - choose_method: ask the user to choose a method (e.g. registration with email) + // - sent_email: the email has been sent to the user + // - passed_challenge: the request was successful and the registration challenge was passed. + // required: true + State State `json:"state" faker:"-" db:"state"` } +var _ flow.Flow = new(Flow) + func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, ft flow.Type) (*Flow, error) { now := time.Now().UTC() id := x.NewUUID() @@ -151,6 +161,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques CSRFToken: csrf, Type: ft, InternalContext: []byte("{}"), + State: flow.StateChooseMethod, }, nil } @@ -238,3 +249,15 @@ func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (o x.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowRegistrationReturnTo(ctx, f.Active.String())), } } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.RegistrationFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/registration/sort.go b/selfservice/flow/registration/sort.go index db44e96274e3..15348dd56512 100644 --- a/selfservice/flow/registration/sort.go +++ b/selfservice/flow/registration/sort.go @@ -16,6 +16,7 @@ func SortNodes(ctx context.Context, n node.Nodes, schemaRef string) error { node.DefaultGroup, node.OpenIDConnectGroup, node.WebAuthnGroup, + node.CodeGroup, node.PasswordGroup, }), node.SortUpdateOrder(node.PasswordLoginOrder), diff --git a/selfservice/flow/registration/state.go b/selfservice/flow/registration/state.go new file mode 100644 index 000000000000..be50551d6850 --- /dev/null +++ b/selfservice/flow/registration/state.go @@ -0,0 +1,15 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import "github.com/ory/kratos/selfservice/flow" + +// State represents the state of this request: +// +// - choose_method: ask the user to choose a method (e.g. registration with email) +// - sent_email: the email has been sent to the user +// - passed_challenge: the request was successful and the registration challenge was passed. +// +// swagger:model registrationFlowState +type State = flow.State diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go index af1b31968caa..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 e41509e19c8e..28a71e47a977 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -106,6 +106,8 @@ type OAuth2LoginChallengeParams struct { AMR session.AuthenticationMethods `db:"authentication_methods" json:"-"` } +var _ flow.Flow = new(Flow) + func (f *Flow) GetType() flow.Type { return f.Type } @@ -144,7 +146,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, } @@ -270,3 +272,15 @@ func (f *Flow) ContinueURL(ctx context.Context, config *config.Config) *url.URL } return returnTo } + +func (f *Flow) GetState() State { + return f.State +} + +func (f *Flow) GetFlowName() flow.FlowName { + return flow.VerificationFlow +} + +func (f *Flow) SetState(state State) { + f.State = state +} diff --git a/selfservice/flow/verification/flow_test.go b/selfservice/flow/verification/flow_test.go index fb21b707e387..3485f3454fc4 100644 --- a/selfservice/flow/verification/flow_test.go +++ b/selfservice/flow/verification/flow_test.go @@ -64,7 +64,7 @@ func TestFlow(t *testing.T) { require.NoError(t, err) }) - assert.EqualValues(t, verification.StateChooseMethod, + assert.EqualValues(t, flow.StateChooseMethod, must(verification.NewFlow(conf, time.Hour, "", u, nil, flow.TypeBrowser)).State) } @@ -207,5 +207,4 @@ func TestContinueURL(t *testing.T) { require.Equal(t, tc.expect, url.String()) }) } - } diff --git a/selfservice/flow/verification/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 ba4d7e7d79a7..e72f833faabf 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -20,8 +20,10 @@ import ( "github.com/ory/x/otelx" ) -var _ registration.PostHookPostPersistExecutor = new(Verifier) -var _ settings.PostHookPostPersistExecutor = new(Verifier) +var ( + _ registration.PostHookPostPersistExecutor = new(Verifier) + _ settings.PostHookPostPersistExecutor = new(Verifier) +) type ( verifierDependencies interface { @@ -100,7 +102,7 @@ func (e *Verifier) do( flowCallback(verificationFlow) } - verificationFlow.State = verification.StateEmailSent + verificationFlow.State = flow.StateEmailSent if err := strategy.PopulateVerificationMethod(r, verificationFlow); err != nil { return err diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 1013de192223..3d4195b6e0ba 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -22,7 +22,6 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" - "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" "github.com/ory/kratos/x" @@ -94,7 +93,7 @@ func TestVerifier(t *testing.T) { expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) require.NoError(t, err) - require.Equal(t, expectedVerificationFlow.State, verification.StateEmailSent) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) require.NoError(t, err) @@ -110,7 +109,7 @@ func TestVerifier(t *testing.T) { // Email to baz@ory.sh is skipped because it is verified already. assert.NotContains(t, recipients, "baz@ory.sh") - //these addresses will be marked as sent and won't be sent again by the settings hook + // these addresses will be marked as sent and won't be sent again by the settings hook address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") require.NoError(t, err) assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json 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 92ac3b66d2a5..e4b6fae22c55 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -177,23 +177,23 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. return } - flow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) + recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) if err != nil { s.deps.Writer().WriteError(w, r, err) return } - flow.DangerousSkipCSRFCheck = true - flow.State = recovery.StateEmailSent - flow.UI.Nodes = node.Nodes{} - flow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + recoveryFlow.DangerousSkipCSRFCheck = true + recoveryFlow.State = flow.StateEmailSent + recoveryFlow.UI.Nodes = node.Nodes{} + recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) - flow.UI.Nodes. + recoveryFlow.UI.Nodes. Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeLabelSubmit())) - if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, flow); err != nil { + if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil { s.deps.Writer().WriteError(w, r, err) return } @@ -213,7 +213,7 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. RawCode: rawCode, CodeType: RecoveryCodeTypeAdmin, ExpiresIn: expiresIn, - FlowID: flow.ID, + FlowID: recoveryFlow.ID, IdentityID: id.ID, }); err != nil { s.deps.Writer().WriteError(w, r, err) @@ -226,11 +226,11 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. Info("A recovery code has been created.") body := &recoveryCodeForIdentity{ - ExpiresAt: flow.ExpiresAt.UTC(), + ExpiresAt: recoveryFlow.ExpiresAt.UTC(), RecoveryLink: urlx.CopyWithQuery( s.deps.Config().SelfServiceFlowRecoveryUI(ctx), url.Values{ - "flow": {flow.ID.String()}, + "flow": {recoveryFlow.ID.String()}, }).String(), RecoveryCode: rawCode, } @@ -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 2adbf97213d4..92e484f324b3 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 6ab3ff4ae904..da4f40ac9516 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -40,7 +40,6 @@ func (s *Strategy) RecoveryStrategyID() string { func (s *Strategy) RegisterPublicRecoveryRoutes(public *x.RouterPublic) { s.d.CSRFHandler().IgnorePath(RouteAdminCreateRecoveryLink) public.POST(RouteAdminCreateRecoveryLink, x.RedirectToAdminRoute(s.d)) - } func (s *Strategy) RegisterAdminRecoveryRoutes(admin *x.RouterAdmin) { @@ -198,7 +197,8 @@ func (s *Strategy) createRecoveryLinkForIdentity(w http.ResponseWriter, r *http. url.Values{ "token": {token.Token}, "flow": {req.ID.String()}, - }).String()}, + }).String(), + }, herodot.UnescapedHTML) } @@ -237,7 +237,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } if len(body.Token) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } @@ -253,7 +253,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return errors.WithStack(flow.ErrCompletedByStrategy) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.RecoveryStrategyID(), body.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), body.Method, s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) } @@ -267,11 +267,11 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch req.State { - case recovery.StateChooseMethod: + case flow.StateChooseMethod: fallthrough - case recovery.StateEmailSent: + case flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, req) - case recovery.StatePassedChallenge: + case flow.StatePassedChallenge: // was already handled, do not allow retry return s.retryRecoveryFlowWithMessage(w, r, req.Type, text.NewErrorValidationRecoveryRetrySuccess()) default: @@ -281,7 +281,7 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, f *recovery.Flow, id *identity.Identity) error { f.UI.Messages.Clear() - f.State = recovery.StatePassedChallenge + f.State = flow.StatePassedChallenge f.SetCSRFToken(s.d.CSRFHandler().RegenerateToken(w, r)) f.RecoveredIdentityID = uuid.NullUUID{ UUID: id.ID, @@ -455,8 +455,8 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.Active = sqlxx.NullString(s.RecoveryNodeGroup()) - f.State = recovery.StateEmailSent + f.Active = sqlxx.NullString(s.NodeGroup()) + f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewRecoveryEmailSent()) if err := s.d.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil { return s.HandleRecoveryError(w, r, f, body, err) diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 4af007577500..07625da76d7e 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 464f4ad5796f..debc45d770db 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -137,12 +137,12 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat f.TransientPayload = p.TransientPayload - var pid = p.Provider // this can come from both url query and post body + pid := p.Provider // this can come from both url query and post body if pid == "" { return errors.WithStack(flow.ErrStrategyNotResponsible) } - if err := flow.MethodEnabledAndAllowed(r.Context(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index d7497e567ab3..69f2bc03a560 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -76,41 +76,59 @@ func TestSettingsStrategy(t *testing.T) { // Make test data for this test run unique testID := x.NewUUID().String() users := map[string]*identity.Identity{ - "password": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"john` + testID + `@doe.com"}`), + "password": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"john` + testID + `@doe.com"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", + "password": { + Type: "password", Identifiers: []string{"john+" + testID + "@doe.com"}, - Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`)}}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + }, }, - "oryer": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+` + testID + `@ory.sh"}`), + "oryer": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+` + testID + `@ory.sh"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, Credentials: map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+` + testID + `"}]}`), + }, + }, }, - "githuber": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+github+` + testID + `@ory.sh"}`), + "githuber": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+github+` + testID + `@ory.sh"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+github+" + testID, "github:hackerman+github+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+github+` + testID + `"},{"provider":"github","subject":"hackerman+github+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+github+` + testID + `"},{"provider":"github","subject":"hackerman+github+` + testID + `"}]}`), + }, + }, SchemaID: config.DefaultIdentityTraitsSchemaID, }, - "multiuser": {ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+multiuser+` + testID + `@ory.sh"}`), + "multiuser": { + ID: x.NewUUID(), Traits: identity.Traits(`{"email":"hackerman+multiuser+` + testID + `@ory.sh"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {Type: "password", + "password": { + Type: "password", Identifiers: []string{"hackerman+multiuser+" + testID + "@ory.sh"}, - Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`)}, - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$argon2id$iammocked...."}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, Identifiers: []string{"ory:hackerman+multiuser+" + testID, "google:hackerman+multiuser+" + testID}, - Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+multiuser+` + testID + `"},{"provider":"google","subject":"hackerman+multiuser+` + testID + `"}]}`)}}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"ory","subject":"hackerman+multiuser+` + testID + `"},{"provider":"google","subject":"hackerman+multiuser+` + testID + `"}]}`), + }, + }, SchemaID: config.DefaultIdentityTraitsSchemaID, }, } agents := testhelpers.AddAndLoginIdentities(t, reg, publicTS, users) - var newProfileFlow = func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *settings.Flow { + newProfileFlow := func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *settings.Flow { req, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), x.ParseUUID(string(testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS).Id))) require.NoError(t, err) @@ -131,7 +149,7 @@ func TestSettingsStrategy(t *testing.T) { } // does the same as new profile request but uses the SDK - var nprSDK = func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *kratos.SettingsFlow { + nprSDK := func(t *testing.T, client *http.Client, redirectTo string, exp time.Duration) *kratos.SettingsFlow { return testhelpers.InitializeSettingsFlowViaBrowser(t, client, false, publicTS) } @@ -208,11 +226,11 @@ func TestSettingsStrategy(t *testing.T) { } }) - var action = func(req *kratos.SettingsFlow) string { + action := func(req *kratos.SettingsFlow) string { return req.Ui.Action } - var checkCredentials = func(t *testing.T, shouldExist bool, iid uuid.UUID, provider, subject string, expectTokens bool) { + checkCredentials := func(t *testing.T, shouldExist bool, iid uuid.UUID, provider, subject string, expectTokens bool) { actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), iid) require.NoError(t, err) @@ -242,7 +260,7 @@ func TestSettingsStrategy(t *testing.T) { require.EqualValues(t, shouldExist, found) } - var reset = func(t *testing.T) func() { + reset := func(t *testing.T) func() { return func() { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Minute*5) agents = testhelpers.AddAndLoginIdentities(t, reg, publicTS, users) @@ -250,20 +268,20 @@ func TestSettingsStrategy(t *testing.T) { } t.Run("suite=unlink", func(t *testing.T) { - var unlink = func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + unlink := func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { req = nprSDK(t, agents[agent], "", time.Hour) body, res = testhelpers.HTTPPostForm(t, agents[agent], action(req), &url.Values{"csrf_token": {x.FakeCSRFToken}, "unlink": {provider}}) return } - var unlinkInvalid = func(agent, provider, errorMessage string) func(t *testing.T) { + unlinkInvalid := func(agent, provider, errorMessage string) func(t *testing.T) { return func(t *testing.T) { body, res, req := unlink(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) - //assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) + // assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) // The original options to link google and github are still there t.Run("flow=fetch", func(t *testing.T) { @@ -302,7 +320,7 @@ func TestSettingsStrategy(t *testing.T) { t.Run("case=should not be able to unlink a connection without a privileged session", func(t *testing.T) { agent, provider := "githuber", "github" - var runUnauthed = func(t *testing.T) *kratos.SettingsFlow { + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) time.Sleep(time.Millisecond) t.Cleanup(reset(t)) @@ -311,7 +329,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateShowForm, rs.State) + require.EqualValues(t, flow.StateShowForm, rs.State) checkCredentials(t, true, users[agent].ID, provider, "hackerman+github+"+testID, false) @@ -340,19 +358,19 @@ func TestSettingsStrategy(t *testing.T) { }) t.Run("suite=link", func(t *testing.T) { - var link = func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { + link := func(t *testing.T, agent, provider string) (body []byte, res *http.Response, req *kratos.SettingsFlow) { req = nprSDK(t, agents[agent], "", time.Hour) body, res = testhelpers.HTTPPostForm(t, agents[agent], action(req), &url.Values{"csrf_token": {x.FakeCSRFToken}, "link": {provider}}) return } - var linkInvalid = func(agent, provider string) func(t *testing.T) { + linkInvalid := func(agent, provider string) func(t *testing.T) { return func(t *testing.T) { body, res, req := link(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/settings?flow="+req.Id) - //assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) + // assert.EqualValues(t, identity.CredentialsTypeOIDC.String(), gjson.GetBytes(body, "active").String()) assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), publicTS.URL+settings.RouteSubmitFlow+"?flow=") // The original options to link google and github are still there @@ -427,7 +445,7 @@ func TestSettingsStrategy(t *testing.T) { updatedFlowSDK, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(originalFlow.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateSuccess, updatedFlowSDK.State) + require.EqualValues(t, flow.StateSuccess, updatedFlowSDK.State) t.Run("flow=original", func(t *testing.T) { snapshotx.SnapshotTExcept(t, originalFlow.Ui.Nodes, []string{"0.attributes.value", "1.attributes.value"}) @@ -454,7 +472,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateSuccess, rs.State) + require.EqualValues(t, flow.StateSuccess, rs.State) snapshotx.SnapshotTExcept(t, rs.Ui.Nodes, []string{"0.attributes.value", "1.attributes.value"}) @@ -529,7 +547,7 @@ func TestSettingsStrategy(t *testing.T) { agent, provider := "githuber", "google" subject = "hackerman+new+google+" + testID - var runUnauthed = func(t *testing.T) *kratos.SettingsFlow { + runUnauthed := func(t *testing.T) *kratos.SettingsFlow { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Millisecond) time.Sleep(time.Millisecond) t.Cleanup(reset(t)) @@ -538,7 +556,7 @@ func TestSettingsStrategy(t *testing.T) { rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) - require.EqualValues(t, settings.StateShowForm, rs.State) + require.EqualValues(t, flow.StateShowForm, rs.State) checkCredentials(t, false, users[agent].ID, provider, subject, true) @@ -675,10 +693,12 @@ func TestPopulateSettingsMethod(t *testing.T) { oidc.NewUnlinkNode("google"), }, withpw: true, - i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ - "google:1234", + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + }, + Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`), }, - Config: []byte(`{"providers":[{"provider":"google","subject":"1234"}]}`)}, }, { c: defaultConfig, @@ -688,11 +708,13 @@ func TestPopulateSettingsMethod(t *testing.T) { oidc.NewUnlinkNode("google"), oidc.NewUnlinkNode("facebook"), }, - i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ - "google:1234", - "facebook:1234", + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "google:1234", + "facebook:1234", + }, + Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), }, - Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`)}, }, } { t.Run("iteration="+strconv.Itoa(k), func(t *testing.T) { diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 30010b80eb54..7a56ebaf45d4 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -51,7 +51,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return nil, err } diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 5fa174f48cbc..b49ff630e458 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -78,7 +78,7 @@ func (s *Strategy) decode(p *UpdateRegistrationFlowWithPasswordMethod, r *http.R } func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { - if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return err } diff --git a/selfservice/strategy/password/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 d1973e17f7fd..3b53d5c4c5f9 --- 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" @@ -422,6 +434,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" @@ -430,6 +468,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": [ @@ -906,6 +953,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": { @@ -1209,6 +1268,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" }, @@ -1227,7 +1289,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "title": "Login Flow", "type": "object" @@ -1285,7 +1348,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "enum": [ "recovery_invalid", "recovery_valid", @@ -1296,10 +1359,12 @@ "verification_code_invalid", "verification_code_valid", "otp", - "stub" + "stub", + "login_code_valid", + "registration_code_valid" ], "type": "string", - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/components/schemas/courierMessageType" @@ -1504,7 +1569,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" @@ -1525,16 +1590,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": { @@ -1623,6 +1678,9 @@ "description": "SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the flow has been completed.\nThis is only set if the client has requested a session token exchange code, and if the flow is of type \"api\",\nand only on creating the flow.", "type": "string" }, + "state": { + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed." + }, "transient_payload": { "description": "TransientPayload is used to pass data from the registration to a webhook", "type": "object" @@ -1640,7 +1698,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "type": "object" }, @@ -1829,7 +1888,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" @@ -1851,15 +1910,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": { @@ -2384,6 +2434,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": { @@ -2612,6 +2692,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": { @@ -3064,7 +3178,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/verificationFlowState" + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -3082,16 +3196,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 01ad8fb3bf27..1a3ccd18cf55 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3076,6 +3076,9 @@ } }, "definitions": { + "CodeAddressType": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -3090,6 +3093,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.", @@ -3396,7 +3417,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", @@ -3847,6 +3884,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.", @@ -4098,7 +4147,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "properties": { "active": { @@ -4150,6 +4200,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" }, @@ -4229,7 +4282,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "type": "string", "enum": [ "recovery_invalid", @@ -4241,9 +4294,11 @@ "verification_code_invalid", "verification_code_valid", "otp", - "stub" + "stub", + "login_code_valid", + "registration_code_valid" ], - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/definitions/courierMessageType" @@ -4437,7 +4492,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" @@ -4447,11 +4502,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": [ @@ -4509,7 +4559,8 @@ "expires_at", "issued_at", "request_url", - "ui" + "ui", + "state" ], "properties": { "active": { @@ -4549,6 +4600,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" @@ -4747,7 +4801,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" @@ -4757,11 +4811,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", @@ -5235,6 +5284,36 @@ "updateLoginFlowBody": { "type": "object" }, + "updateLoginFlowWithCodeMethod": { + "description": "Update Login flow using the code method", + "type": "object", + "required": [ + "method", + "csrf_token" + ], + "properties": { + "code": { + "description": "Code is the 6 digits code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "identifier": { + "description": "Identifier is the code identifier\nThe identifier requires that the user has already completed the registration or settings with code flow.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"code\" when logging in using the code strategy.", + "type": "string" + }, + "resend": { + "description": "Resend is set when the user wants to resend the code", + "type": "string" + } + } + }, "updateLoginFlowWithLookupSecretMethod": { "description": "Update Login Flow with Lookup Secret Method", "type": "object", @@ -5431,6 +5510,40 @@ "description": "Update Registration Request Body", "type": "object" }, + "updateRegistrationFlowWithCodeMethod": { + "description": "Update Registration Flow with Code Method", + "type": "object", + "required": [ + "traits", + "method" + ], + "properties": { + "code": { + "description": "The OTP Code sent to the user", + "type": "string" + }, + "csrf_token": { + "description": "The CSRF Token", + "type": "string" + }, + "method": { + "description": "Method to use\n\nThis field must be set to `code` when using the code method.", + "type": "string" + }, + "resend": { + "description": "Resend restarts the flow with a new code", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, "updateRegistrationFlowWithOidcMethod": { "description": "Update Registration Flow with OpenID Connect Method", "type": "object", @@ -5844,7 +5957,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" @@ -5854,11 +5967,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 9ab3ea5a41ff..566a7393e271 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 6119a2b61f8b..deb35875b725 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,8 +136,8 @@ prepare() { nc -zv localhost 4445 && exit 1 nc -zv localhost 4446 && exit 1 nc -zv localhost 4455 && exit 1 - nc -zv localhost 4456 && exit 1 nc -zv localhost 19006 && exit 1 + # nc -zv localhost 4456 && exit 1 nc -zv localhost 4458 && exit 1 nc -zv localhost 4744 && exit 1 nc -zv localhost 4745 && exit 1 @@ -219,19 +219,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 ( @@ -273,7 +273,7 @@ run() { nc -zv localhost 4433 && exit 1 ls -la . - for profile in email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do + for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do yq ea '. as $item ireduce ({}; . * $item )' test/e2e/profiles/kratos.base.yml "test/e2e/profiles/${profile}/.kratos.yml" > test/e2e/kratos.${profile}.yml cat "test/e2e/kratos.${profile}.yml" | envsubst | sponge "test/e2e/kratos.${profile}.yml" done diff --git a/text/id.go b/text/id.go index 0716ad991d8d..c0274db092ef 100644 --- a/text/id.go +++ b/text/id.go @@ -23,6 +23,8 @@ const ( InfoSelfServiceLoginContinueWebAuthn // 1010011 InfoSelfServiceLoginWebAuthnPasswordless // 1010012 InfoSelfServiceLoginContinue // 1010013 + InfoSelfServiceLoginEmailWithCodeSent // 1010014 + InfoSelfServiceLoginCode // 1010015 ) const ( @@ -34,11 +36,13 @@ const ( ) const ( - InfoSelfServiceRegistrationRoot ID = 1040000 + iota // 1040000 - InfoSelfServiceRegistration // 1040001 - InfoSelfServiceRegistrationWith // 1040002 - InfoSelfServiceRegistrationContinue // 1040003 - InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 + InfoSelfServiceRegistrationRoot ID = 1040000 + iota // 1040000 + InfoSelfServiceRegistration // 1040001 + InfoSelfServiceRegistrationWith // 1040002 + InfoSelfServiceRegistrationContinue // 1040003 + InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 + InfoSelfServiceRegistrationEmailWithCodeSent // 1040005 + InfoSelfServiceRegistrationRegisterCode // 1040006 ) const ( @@ -83,6 +87,8 @@ const ( InfoNodeLabelContinue // 1070009 InfoNodeLabelRecoveryCode // 1070010 InfoNodeLabelVerificationCode // 1070011 + InfoNodeLabelRegistrationCode // 1070012 + InfoNodeLabelLoginCode // 1070013 ) const ( @@ -128,21 +134,27 @@ const ( ErrorValidationPasswordMinLength ErrorValidationPasswordMaxLength ErrorValidationPasswordTooManyBreaches + ErrorValidationNoCodeUser + ErrorValidationTraitsMismatch ) const ( - ErrorValidationLogin ID = 4010000 + iota // 4010000 - ErrorValidationLoginFlowExpired // 4010001 - ErrorValidationLoginNoStrategyFound // 4010002 - ErrorValidationRegistrationNoStrategyFound // 4010003 - ErrorValidationSettingsNoStrategyFound // 4010004 - ErrorValidationRecoveryNoStrategyFound // 4010005 - ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLogin ID = 4010000 + iota // 4010000 + ErrorValidationLoginFlowExpired // 4010001 + ErrorValidationLoginNoStrategyFound // 4010002 + ErrorValidationRegistrationNoStrategyFound // 4010003 + ErrorValidationSettingsNoStrategyFound // 4010004 + ErrorValidationRecoveryNoStrategyFound // 4010005 + ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLoginRetrySuccess // 4010007 + ErrorValidationLoginCodeInvalidOrAlreadyUsed // 4010008 ) const ( - ErrorValidationRegistration ID = 4040000 + iota - ErrorValidationRegistrationFlowExpired + ErrorValidationRegistration ID = 4040000 + iota + ErrorValidationRegistrationFlowExpired // 4040001 + ErrorValidateionRegistrationRetrySuccess // 4040002 + ErrorValidationRegistrationCodeInvalidOrAlreadyUsed // 4040003 ) const ( diff --git a/text/message_login.go b/text/message_login.go index 8f69fa1989a8..d5f5243be71e 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -183,3 +183,38 @@ func NewInfoSelfServiceLoginContinue() *Message { Type: Info, } } + +func NewLoginEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceLoginEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the login.", + Context: context(nil), + } +} + +func NewErrorValidationLoginCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationLoginCodeInvalidOrAlreadyUsed, + Text: "The login code is invalid or has already been used. Please try again.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationLoginRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidationLoginRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + Context: context(nil), + } +} + +func NewInfoSelfServiceLoginCode() *Message { + return &Message{ + ID: InfoSelfServiceLoginCode, + Type: Info, + Text: "Sign in with code", + } +} diff --git a/text/message_node.go b/text/message_node.go index f3712ea75b6d..d9f3a03a0009 100644 --- a/text/message_node.go +++ b/text/message_node.go @@ -27,6 +27,22 @@ func NewInfoNodeLabelRecoveryCode() *Message { } } +func NewInfoNodeLabelRegistrationCode() *Message { + return &Message{ + ID: InfoNodeLabelRegistrationCode, + Text: "Registration code", + Type: Info, + } +} + +func NewInfoNodeLabelLoginCode() *Message { + return &Message{ + ID: InfoNodeLabelLoginCode, + Text: "Login code", + Type: Info, + } +} + func NewInfoNodeInputPassword() *Message { return &Message{ ID: InfoNodeLabelInputPassword, diff --git a/text/message_registration.go b/text/message_registration.go index 96fd9d4326b5..06ee4b08df94 100644 --- a/text/message_registration.go +++ b/text/message_registration.go @@ -54,3 +54,38 @@ func NewInfoSelfServiceRegistrationRegisterWebAuthn() *Message { Type: Info, } } + +func NewRegistrationEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the registration.", + Context: context(nil), + } +} + +func NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed() *Message { + return &Message{ + ID: ErrorValidationRegistrationCodeInvalidOrAlreadyUsed, + Text: "The registration code is invalid or has already been used. Please try again.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationRegistrationRetrySuccessful() *Message { + return &Message{ + ID: ErrorValidateionRegistrationRetrySuccess, + Type: Error, + Text: "The request was already completed successfully and can not be retried.", + Context: context(nil), + } +} + +func NewInfoSelfServiceRegistrationRegisterCode() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterCode, + Text: "Sign up with code", + Type: Info, + } +} diff --git a/text/message_validation.go b/text/message_validation.go index 3467261bb68d..8fb2391bfdce 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -366,3 +366,21 @@ func NewErrorValidationSuchNoWebAuthnUser() *Message { Context: context(nil), } } + +func NewErrorValidationNoCodeUser() *Message { + return &Message{ + ID: ErrorValidationNoCodeUser, + Text: "This account does not exist or has not setup sign in with code.", + Type: Error, + Context: context(nil), + } +} + +func NewErrorValidationTraitsMismatch() *Message { + return &Message{ + ID: ErrorValidationTraitsMismatch, + Text: "The provided traits do not match the traits previously associated with this flow.", + Type: Error, + Context: context(nil), + } +} diff --git a/ui/container/container.go b/ui/container/container.go index 8c1b4a0b7a47..d80683cd2800 100644 --- a/ui/container/container.go +++ b/ui/container/container.go @@ -190,7 +190,7 @@ func (c *Container) ParseError(group node.UiNodeGroup, err error) error { default: // The pointer can be ignored because if there is an error, we'll just use // the empty field (global error). - var causes = e.Causes + causes := e.Causes if len(e.Causes) == 0 { pointer, _ := jsonschemax.JSONPointerToDotNotation(e.InstancePtr) c.AddMessage(group, translateValidationError(e), pointer) @@ -339,6 +339,7 @@ func (c *Container) AddMessage(group node.UiNodeGroup, err *text.Message, setFor func (c *Container) Scan(value interface{}) error { return sqlxx.JSONScan(c, value) } + func (c *Container) Value() (driver.Value, error) { return sqlxx.JSONValue(c) } diff --git a/x/xsql/sql.go b/x/xsql/sql.go index f2354d082ca1..7ee2591bcd74 100644 --- a/x/xsql/sql.go +++ b/x/xsql/sql.go @@ -28,6 +28,8 @@ import ( func CleanSQL(t testing.TB, c *pop.Connection) { ctx := context.Background() for _, table := range []string{ + new(code.LoginCode).TableName(ctx), + new(code.RegistrationCode).TableName(ctx), new(continuity.Container).TableName(ctx), new(courier.MessageDispatch).TableName(), new(courier.Message).TableName(ctx), From 6eb0ab0adf1762f3ca7db4fbc07bbc6fffa6fe13 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 16 Aug 2023 15:34:40 +0200 Subject: [PATCH 03/24] fix: flow states --- internal/client-go/.openapi-generator/FILES | 4 +- internal/client-go/README.md | 2 +- .../model_identity_credentials_code.go} | 58 +++---- internal/client-go/model_login_flow.go | 2 +- internal/client-go/model_login_flow_state.go | 4 +- .../client-go/model_recovery_flow_state.go | 4 +- .../model_registration_flow_state.go | 4 +- .../client-go/model_settings_flow_state.go | 4 +- .../model_verification_flow_state.go | 4 +- internal/httpclient/.openapi-generator/FILES | 4 +- internal/httpclient/README.md | 2 +- .../model_identity_credentials_code.go | 162 ++++++++++++++++++ internal/httpclient/model_login_flow.go | 2 +- internal/httpclient/model_login_flow_state.go | 4 +- .../httpclient/model_recovery_flow_state.go | 4 +- .../model_registration_flow_state.go | 4 +- .../httpclient/model_settings_flow_state.go | 4 +- .../model_verification_flow_state.go | 4 +- .../00b1517f-2467-4aaf-b0a5-82b4a27dcaf5.json | 2 +- .../69c80296-36cd-4afc-921a-15369cac5bf0.json | 2 +- selfservice/flow/recovery/test/persistence.go | 11 +- selfservice/flow/request.go | 2 + selfservice/flow/settings/test/persistence.go | 6 +- selfservice/flow/state.go | 51 ++++++ .../flow/verification/test/persistence.go | 10 +- selfservice/strategy/code/test/persistence.go | 8 +- selfservice/strategy/link/test/persistence.go | 8 +- ...ebauthn_login_is_invalid-type=browser.json | 2 +- ...if_webauthn_login_is_invalid-type=spa.json | 2 +- spec/api.json | 94 +++++----- spec/swagger.json | 46 ++--- 31 files changed, 375 insertions(+), 145 deletions(-) rename internal/{httpclient/model_identity_credentials_otp.go => client-go/model_identity_credentials_code.go} (62%) create mode 100644 internal/httpclient/model_identity_credentials_code.go diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index ce02e19c5665..959499576b3d 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -34,7 +34,7 @@ docs/HealthStatus.md docs/Identity.md docs/IdentityApi.md docs/IdentityCredentials.md -docs/IdentityCredentialsOTP.md +docs/IdentityCredentialsCode.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -149,9 +149,9 @@ model_health_not_ready_status.go model_health_status.go model_identity.go model_identity_credentials.go +model_identity_credentials_code.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go -model_identity_credentials_otp.go model_identity_credentials_password.go model_identity_credentials_type.go model_identity_patch.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index 76c437bd3751..084b578785ee 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -159,7 +159,7 @@ Class | Method | HTTP request | Description - [HealthStatus](docs/HealthStatus.md) - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) - - [IdentityCredentialsOTP](docs/IdentityCredentialsOTP.md) + - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) diff --git a/internal/httpclient/model_identity_credentials_otp.go b/internal/client-go/model_identity_credentials_code.go similarity index 62% rename from internal/httpclient/model_identity_credentials_otp.go rename to internal/client-go/model_identity_credentials_code.go index b60601987e67..b3a2914dec14 100644 --- a/internal/httpclient/model_identity_credentials_otp.go +++ b/internal/client-go/model_identity_credentials_code.go @@ -16,31 +16,31 @@ import ( "time" ) -// IdentityCredentialsOTP CredentialsOTP represents an OTP code -type IdentityCredentialsOTP struct { +// IdentityCredentialsCode CredentialsCode represents a one time login/registraiton code +type IdentityCredentialsCode struct { AddressType *string `json:"address_type,omitempty"` UsedAt NullableTime `json:"used_at,omitempty"` } -// NewIdentityCredentialsOTP instantiates a new IdentityCredentialsOTP object +// NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewIdentityCredentialsOTP() *IdentityCredentialsOTP { - this := IdentityCredentialsOTP{} +func NewIdentityCredentialsCode() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} return &this } -// NewIdentityCredentialsOTPWithDefaults instantiates a new IdentityCredentialsOTP object +// NewIdentityCredentialsCodeWithDefaults instantiates a new IdentityCredentialsCode object // This constructor will only assign default values to properties that have it defined, // but it doesn't guarantee that properties required by API are set -func NewIdentityCredentialsOTPWithDefaults() *IdentityCredentialsOTP { - this := IdentityCredentialsOTP{} +func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} return &this } // GetAddressType returns the AddressType field value if set, zero value otherwise. -func (o *IdentityCredentialsOTP) GetAddressType() string { +func (o *IdentityCredentialsCode) GetAddressType() string { if o == nil || o.AddressType == nil { var ret string return ret @@ -50,7 +50,7 @@ func (o *IdentityCredentialsOTP) GetAddressType() string { // 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) { +func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { if o == nil || o.AddressType == nil { return nil, false } @@ -58,7 +58,7 @@ func (o *IdentityCredentialsOTP) GetAddressTypeOk() (*string, bool) { } // HasAddressType returns a boolean if a field has been set. -func (o *IdentityCredentialsOTP) HasAddressType() bool { +func (o *IdentityCredentialsCode) HasAddressType() bool { if o != nil && o.AddressType != nil { return true } @@ -67,12 +67,12 @@ func (o *IdentityCredentialsOTP) HasAddressType() bool { } // SetAddressType gets a reference to the given string and assigns it to the AddressType field. -func (o *IdentityCredentialsOTP) SetAddressType(v string) { +func (o *IdentityCredentialsCode) SetAddressType(v string) { o.AddressType = &v } // GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *IdentityCredentialsOTP) GetUsedAt() time.Time { +func (o *IdentityCredentialsCode) GetUsedAt() time.Time { if o == nil || o.UsedAt.Get() == nil { var ret time.Time return ret @@ -83,7 +83,7 @@ func (o *IdentityCredentialsOTP) GetUsedAt() time.Time { // 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) { +func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { if o == nil { return nil, false } @@ -91,7 +91,7 @@ func (o *IdentityCredentialsOTP) GetUsedAtOk() (*time.Time, bool) { } // HasUsedAt returns a boolean if a field has been set. -func (o *IdentityCredentialsOTP) HasUsedAt() bool { +func (o *IdentityCredentialsCode) HasUsedAt() bool { if o != nil && o.UsedAt.IsSet() { return true } @@ -100,21 +100,21 @@ func (o *IdentityCredentialsOTP) HasUsedAt() bool { } // SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. -func (o *IdentityCredentialsOTP) SetUsedAt(v time.Time) { +func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { o.UsedAt.Set(&v) } // SetUsedAtNil sets the value for UsedAt to be an explicit nil -func (o *IdentityCredentialsOTP) SetUsedAtNil() { +func (o *IdentityCredentialsCode) SetUsedAtNil() { o.UsedAt.Set(nil) } // UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil -func (o *IdentityCredentialsOTP) UnsetUsedAt() { +func (o *IdentityCredentialsCode) UnsetUsedAt() { o.UsedAt.Unset() } -func (o IdentityCredentialsOTP) MarshalJSON() ([]byte, error) { +func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.AddressType != nil { toSerialize["address_type"] = o.AddressType @@ -125,38 +125,38 @@ func (o IdentityCredentialsOTP) MarshalJSON() ([]byte, error) { return json.Marshal(toSerialize) } -type NullableIdentityCredentialsOTP struct { - value *IdentityCredentialsOTP +type NullableIdentityCredentialsCode struct { + value *IdentityCredentialsCode isSet bool } -func (v NullableIdentityCredentialsOTP) Get() *IdentityCredentialsOTP { +func (v NullableIdentityCredentialsCode) Get() *IdentityCredentialsCode { return v.value } -func (v *NullableIdentityCredentialsOTP) Set(val *IdentityCredentialsOTP) { +func (v *NullableIdentityCredentialsCode) Set(val *IdentityCredentialsCode) { v.value = val v.isSet = true } -func (v NullableIdentityCredentialsOTP) IsSet() bool { +func (v NullableIdentityCredentialsCode) IsSet() bool { return v.isSet } -func (v *NullableIdentityCredentialsOTP) Unset() { +func (v *NullableIdentityCredentialsCode) Unset() { v.value = nil v.isSet = false } -func NewNullableIdentityCredentialsOTP(val *IdentityCredentialsOTP) *NullableIdentityCredentialsOTP { - return &NullableIdentityCredentialsOTP{value: val, isSet: true} +func NewNullableIdentityCredentialsCode(val *IdentityCredentialsCode) *NullableIdentityCredentialsCode { + return &NullableIdentityCredentialsCode{value: val, isSet: true} } -func (v NullableIdentityCredentialsOTP) MarshalJSON() ([]byte, error) { +func (v NullableIdentityCredentialsCode) MarshalJSON() ([]byte, error) { return json.Marshal(v.value) } -func (v *NullableIdentityCredentialsOTP) UnmarshalJSON(src []byte) error { +func (v *NullableIdentityCredentialsCode) 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 fb27ee68236d..dc7c67ea2649 100644 --- a/internal/client-go/model_login_flow.go +++ b/internal/client-go/model_login_flow.go @@ -39,7 +39,7 @@ 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 represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` diff --git a/internal/client-go/model_login_flow_state.go b/internal/client-go/model_login_flow_state.go index 0dd0545f031c..ce5570b79032 100644 --- a/internal/client-go/model_login_flow_state.go +++ b/internal/client-go/model_login_flow_state.go @@ -19,7 +19,7 @@ import ( // 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 +// List of loginFlowState const ( LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *LoginFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid LoginFlowState", value) } -// Ptr returns reference to LoginFlowState value +// Ptr returns reference to loginFlowState value func (v LoginFlowState) Ptr() *LoginFlowState { return &v } diff --git a/internal/client-go/model_recovery_flow_state.go b/internal/client-go/model_recovery_flow_state.go index 53f95534661d..1c660ba043b9 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_state.go b/internal/client-go/model_registration_flow_state.go index c3be9f33cd79..86f3fd38cff0 100644 --- a/internal/client-go/model_registration_flow_state.go +++ b/internal/client-go/model_registration_flow_state.go @@ -19,7 +19,7 @@ import ( // 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 +// List of registrationFlowState const ( REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) } -// Ptr returns reference to RegistrationFlowState value +// Ptr returns reference to registrationFlowState value func (v RegistrationFlowState) Ptr() *RegistrationFlowState { return &v } diff --git a/internal/client-go/model_settings_flow_state.go b/internal/client-go/model_settings_flow_state.go index 6d9e5b93f1fe..f994c786a2d8 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_verification_flow_state.go b/internal/client-go/model_verification_flow_state.go index b34326eec3fc..bea74568c94d 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 59f80226cbdb..7018bd681eac 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -35,7 +35,7 @@ docs/HealthStatus.md docs/Identity.md docs/IdentityApi.md docs/IdentityCredentials.md -docs/IdentityCredentialsOTP.md +docs/IdentityCredentialsCode.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -150,9 +150,9 @@ model_health_not_ready_status.go model_health_status.go model_identity.go model_identity_credentials.go +model_identity_credentials_code.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go -model_identity_credentials_otp.go model_identity_credentials_password.go model_identity_credentials_type.go model_identity_patch.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 76c437bd3751..084b578785ee 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -159,7 +159,7 @@ Class | Method | HTTP request | Description - [HealthStatus](docs/HealthStatus.md) - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) - - [IdentityCredentialsOTP](docs/IdentityCredentialsOTP.md) + - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) diff --git a/internal/httpclient/model_identity_credentials_code.go b/internal/httpclient/model_identity_credentials_code.go new file mode 100644 index 000000000000..b3a2914dec14 --- /dev/null +++ b/internal/httpclient/model_identity_credentials_code.go @@ -0,0 +1,162 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "time" +) + +// IdentityCredentialsCode CredentialsCode represents a one time login/registraiton code +type IdentityCredentialsCode struct { + AddressType *string `json:"address_type,omitempty"` + UsedAt NullableTime `json:"used_at,omitempty"` +} + +// NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsCode() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// NewIdentityCredentialsCodeWithDefaults instantiates a new IdentityCredentialsCode object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { + this := IdentityCredentialsCode{} + return &this +} + +// GetAddressType returns the AddressType field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddressType() string { + if o == nil || o.AddressType == nil { + var ret string + return ret + } + return *o.AddressType +} + +// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { + if o == nil || o.AddressType == nil { + return nil, false + } + return o.AddressType, true +} + +// HasAddressType returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddressType() bool { + if o != nil && o.AddressType != nil { + return true + } + + return false +} + +// SetAddressType gets a reference to the given string and assigns it to the AddressType field. +func (o *IdentityCredentialsCode) SetAddressType(v string) { + o.AddressType = &v +} + +// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityCredentialsCode) GetUsedAt() time.Time { + if o == nil || o.UsedAt.Get() == nil { + var ret time.Time + return ret + } + return *o.UsedAt.Get() +} + +// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return o.UsedAt.Get(), o.UsedAt.IsSet() +} + +// HasUsedAt returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasUsedAt() bool { + if o != nil && o.UsedAt.IsSet() { + return true + } + + return false +} + +// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. +func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { + o.UsedAt.Set(&v) +} + +// SetUsedAtNil sets the value for UsedAt to be an explicit nil +func (o *IdentityCredentialsCode) SetUsedAtNil() { + o.UsedAt.Set(nil) +} + +// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil +func (o *IdentityCredentialsCode) UnsetUsedAt() { + o.UsedAt.Unset() +} + +func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.AddressType != nil { + toSerialize["address_type"] = o.AddressType + } + if o.UsedAt.IsSet() { + toSerialize["used_at"] = o.UsedAt.Get() + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsCode struct { + value *IdentityCredentialsCode + isSet bool +} + +func (v NullableIdentityCredentialsCode) Get() *IdentityCredentialsCode { + return v.value +} + +func (v *NullableIdentityCredentialsCode) Set(val *IdentityCredentialsCode) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCode) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCode(val *IdentityCredentialsCode) *NullableIdentityCredentialsCode { + return &NullableIdentityCredentialsCode{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_login_flow.go b/internal/httpclient/model_login_flow.go index fb27ee68236d..dc7c67ea2649 100644 --- a/internal/httpclient/model_login_flow.go +++ b/internal/httpclient/model_login_flow.go @@ -39,7 +39,7 @@ 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 represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. State interface{} `json:"state"` // The flow type can either be `api` or `browser`. Type string `json:"type"` diff --git a/internal/httpclient/model_login_flow_state.go b/internal/httpclient/model_login_flow_state.go index 0dd0545f031c..ce5570b79032 100644 --- a/internal/httpclient/model_login_flow_state.go +++ b/internal/httpclient/model_login_flow_state.go @@ -19,7 +19,7 @@ import ( // 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 +// List of loginFlowState const ( LOGINFLOWSTATE_CHOOSE_METHOD LoginFlowState = "choose_method" LOGINFLOWSTATE_SENT_EMAIL LoginFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *LoginFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid LoginFlowState", value) } -// Ptr returns reference to LoginFlowState value +// Ptr returns reference to loginFlowState value func (v LoginFlowState) Ptr() *LoginFlowState { return &v } diff --git a/internal/httpclient/model_recovery_flow_state.go b/internal/httpclient/model_recovery_flow_state.go index 53f95534661d..1c660ba043b9 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_state.go b/internal/httpclient/model_registration_flow_state.go index c3be9f33cd79..86f3fd38cff0 100644 --- a/internal/httpclient/model_registration_flow_state.go +++ b/internal/httpclient/model_registration_flow_state.go @@ -19,7 +19,7 @@ import ( // 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 +// List of registrationFlowState const ( REGISTRATIONFLOWSTATE_CHOOSE_METHOD RegistrationFlowState = "choose_method" REGISTRATIONFLOWSTATE_SENT_EMAIL RegistrationFlowState = "sent_email" @@ -43,7 +43,7 @@ func (v *RegistrationFlowState) UnmarshalJSON(src []byte) error { return fmt.Errorf("%+v is not a valid RegistrationFlowState", value) } -// Ptr returns reference to RegistrationFlowState value +// Ptr returns reference to registrationFlowState value func (v RegistrationFlowState) Ptr() *RegistrationFlowState { return &v } diff --git a/internal/httpclient/model_settings_flow_state.go b/internal/httpclient/model_settings_flow_state.go index 6d9e5b93f1fe..f994c786a2d8 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_verification_flow_state.go b/internal/httpclient/model_verification_flow_state.go index b34326eec3fc..bea74568c94d 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/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 index 35690d9d954b..17f770efbe90 100644 --- 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 @@ -14,5 +14,5 @@ "updated_at": "2013-10-07T08:23:19Z", "refresh": false, "requested_aal": "aal1", - "state": "" + "state": "choose_method" } 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 index e5fcc4a278f1..6179619c71fe 100644 --- 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 @@ -10,5 +10,5 @@ "method": "", "nodes": null }, - "state": "" + "state": "choose_method" } diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go index 91e8a2c763b3..665dc84b9130 100644 --- a/selfservice/flow/recovery/test/persistence.go +++ b/selfservice/flow/recovery/test/persistence.go @@ -15,6 +15,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" @@ -24,8 +25,9 @@ import ( func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { - var clearids = func(r *recovery.Flow) { +}, +) func(t *testing.T) { + clearids := func(r *recovery.Flow) { r.ID = uuid.UUID{} } @@ -38,10 +40,11 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { require.Error(t, err) }) - var newFlow = func(t *testing.T) *recovery.Flow { + newFlow := func(t *testing.T) *recovery.Flow { var r recovery.Flow require.NoError(t, faker.FakeData(&r)) clearids(&r) + r.State = flow.StateShowForm return &r } @@ -61,6 +64,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { t.Run("case=should create with set ids", func(t *testing.T) { var r recovery.Flow require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateShowForm require.NoError(t, p.CreateRecoveryFlow(ctx, &r)) require.Equal(t, nid, r.NID) @@ -162,7 +166,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("case=handle network reference issues", func(t *testing.T) { - }) } } diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go index 1a3b091ed548..bb595351d8b2 100644 --- a/selfservice/flow/request.go +++ b/selfservice/flow/request.go @@ -117,6 +117,8 @@ func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, a ok = d.Config().SelfServiceCodeStrategy(ctx).RegistrationEnabled case LoginFlow: ok = d.Config().SelfServiceCodeStrategy(ctx).LoginEnabled + case VerificationFlow, RecoveryFlow: + ok = d.Config().SelfServiceCodeStrategy(ctx).Enabled default: ok = false } diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go index 82b56f74d6e5..62bce9c547bf 100644 --- a/selfservice/flow/settings/test/persistence.go +++ b/selfservice/flow/settings/test/persistence.go @@ -14,6 +14,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/x/sqlcon" @@ -54,6 +55,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P require.NoError(t, p.CreateIdentity(ctx, r.Identity)) require.NotZero(t, r.Identity.ID) r.IdentityID = r.Identity.ID + r.State = flow.StateShowForm return &r } @@ -106,6 +108,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P t.Run("case=should create with set ids", func(t *testing.T) { var r settings.Flow require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateShowForm require.NoError(t, p.CreateIdentity(ctx, r.Identity)) require.NoError(t, p.CreateSettingsFlow(ctx, &r)) @@ -146,6 +149,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P clearids(&expected) expected.Identity = nil expected.IdentityID = uuid.Nil + expected.State = flow.StateShowForm err := p.CreateSettingsFlow(ctx, &expected) require.Errorf(t, err, "%+v", expected) }) @@ -201,7 +205,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.P require.NoError(t, p.CreateIdentity(ctx, &identity.Identity{ID: iid})) t.Run("sets id on creation", func(t *testing.T) { - expected := &settings.Flow{ID: id, IdentityID: iid, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)} + expected := &settings.Flow{ID: id, IdentityID: iid, State: flow.StateShowForm, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)} require.NoError(t, p.CreateSettingsFlow(ctx, expected)) assert.EqualValues(t, id, expected.ID) assert.EqualValues(t, nid, expected.NID) diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go index 3ef8ed5dbe03..76a0683fc19d 100644 --- a/selfservice/flow/state.go +++ b/selfservice/flow/state.go @@ -3,6 +3,14 @@ package flow +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + + "github.com/pkg/errors" +) + // Flow State // // The state represents the state of the verification flow. @@ -47,3 +55,46 @@ func NextState(current State) State { return states[indexOf(current)+1] } + +// For some reason using sqlxx.NullString as the State type does not work here. +// Reimplementing the Scanner interface on type State does work and allows +// the state to be NULL in the database. + +// MarshalJSON returns m as the JSON encoding of m. +func (ns State) MarshalJSON() ([]byte, error) { + return json.Marshal(string(ns)) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *State) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal(data, (*string)(ns))) +} + +// Scan implements the Scanner interface. +func (ns *State) Scan(value interface{}) error { + var v sql.NullString + if err := (&v).Scan(value); err != nil { + return err + } + *ns = State(v.String) + return nil +} + +// Value implements the driver Valuer interface. +func (ns State) Value() (driver.Value, error) { + if len(ns) == 0 { + return sql.NullString{}.Value() + } + return sql.NullString{Valid: true, String: string(ns)}.Value() +} + +// String implements the Stringer interface. +func (ns State) String() string { + return string(ns) +} diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go index dac7f539e334..0358462029f7 100644 --- a/selfservice/flow/verification/test/persistence.go +++ b/selfservice/flow/verification/test/persistence.go @@ -15,6 +15,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" @@ -24,8 +25,9 @@ import ( func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { - var clearids = func(r *verification.Flow) { +}, +) func(t *testing.T) { + clearids := func(r *verification.Flow) { r.ID = uuid.UUID{} } @@ -39,10 +41,11 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { require.Error(t, err) }) - var newFlow = func(t *testing.T) *verification.Flow { + newFlow := func(t *testing.T) *verification.Flow { var r verification.Flow require.NoError(t, faker.FakeData(&r)) clearids(&r) + r.State = flow.StateChooseMethod return &r } @@ -62,6 +65,7 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { t.Run("case=should create with set ids", func(t *testing.T) { var r verification.Flow require.NoError(t, faker.FakeData(&r)) + r.State = flow.StateChooseMethod require.NoError(t, p.CreateVerificationFlow(ctx, &r)) require.Equal(t, nid, r.NID) diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index cf8e25afc155..f505bcb9c185 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -10,6 +10,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/x/randx" @@ -25,7 +26,8 @@ import ( func TestPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { +}, +) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) @@ -33,10 +35,11 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("code=recovery", func(t *testing.T) { - newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) { var f recovery.Flow require.NoError(t, faker.FakeData(&f)) + f.State = flow.StateChooseMethod + require.NoError(t, p.CreateRecoveryFlow(ctx, &f)) var i identity.Identity @@ -141,7 +144,6 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { count, err = p.GetConnection(ctx).Where("selfservice_recovery_flow_id = ?", f.ID).Count(&code.RecoveryCode{}) require.NoError(t, err) require.Equal(t, 0, count) - }) }) } diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index ae669a888453..63abe6179f18 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -10,6 +10,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/x/sqlcon" @@ -29,7 +30,8 @@ import ( func TestPersister(ctx context.Context, conf *config.Config, p interface { persistence.Persister -}) func(t *testing.T) { +}, +) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) @@ -37,10 +39,10 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("token=recovery", func(t *testing.T) { - newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) { var req recovery.Flow require.NoError(t, faker.FakeData(&req)) + req.State = flow.StateChooseMethod require.NoError(t, p.CreateRecoveryFlow(ctx, &req)) var i identity.Identity @@ -122,13 +124,13 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { require.Error(t, err) }) }) - }) t.Run("token=verification", func(t *testing.T) { newVerificationToken := func(t *testing.T, email string) (*verification.Flow, *link.VerificationToken) { var f verification.Flow require.NoError(t, faker.FakeData(&f)) + f.State = flow.StateChooseMethod require.NoError(t, p.CreateVerificationFlow(ctx, &f)) var i identity.Identity diff --git a/selfservice/strategy/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 342c8a3ab76f..a12f8fc0e182 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -83,5 +83,5 @@ }, "refresh": false, "requested_aal": "aal1", - "state": "" + "state": "choose_method" } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index 342c8a3ab76f..a12f8fc0e182 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -83,5 +83,5 @@ }, "refresh": false, "requested_aal": "aal1", - "state": "" + "state": "choose_method" } diff --git a/spec/api.json b/spec/api.json index 3b53d5c4c5f9..4e6c2bbeda7d 100644 --- a/spec/api.json +++ b/spec/api.json @@ -98,15 +98,6 @@ "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" @@ -434,32 +425,6 @@ "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" @@ -468,15 +433,6 @@ "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": [ @@ -953,8 +909,8 @@ }, "type": "object" }, - "identityCredentialsOTP": { - "description": "CredentialsOTP represents an OTP code", + "identityCredentialsCode": { + "description": "CredentialsCode represents a one time login/registraiton code", "properties": { "address_type": { "$ref": "#/components/schemas/CodeAddressType" @@ -1269,7 +1225,7 @@ "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." + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" @@ -1295,6 +1251,15 @@ "title": "Login Flow", "type": "object" }, + "loginFlowState": { + "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "Login Flow State" + }, "logoutFlow": { "description": "Logout Flow", "properties": { @@ -1590,6 +1555,15 @@ "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" + }, "recoveryIdentityAddress": { "properties": { "created_at": { @@ -1703,6 +1677,15 @@ ], "type": "object" }, + "registrationFlowState": { + "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", + "enum": [ + "choose_method", + "sent_email", + "passed_challenge" + ], + "title": "State represents the state of this request:" + }, "selfServiceFlowExpiredError": { "description": "Is sent when a flow is expired", "properties": { @@ -1910,6 +1893,14 @@ "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:" + }, "successfulCodeExchangeResponse": { "description": "The Response for Registration Flows via API", "properties": { @@ -3196,6 +3187,15 @@ "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" + }, "version": { "properties": { "version": { diff --git a/spec/swagger.json b/spec/swagger.json index 1a3ccd18cf55..63e280fc3da5 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3093,10 +3093,6 @@ "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", @@ -3417,23 +3413,7 @@ "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", @@ -3884,8 +3864,8 @@ } } }, - "identityCredentialsOTP": { - "description": "CredentialsOTP represents an OTP code", + "identityCredentialsCode": { + "description": "CredentialsCode represents a one time login/registraiton code", "type": "object", "properties": { "address_type": { @@ -4201,7 +4181,7 @@ "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." + "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." }, "type": { "$ref": "#/definitions/selfServiceFlowType" @@ -4216,6 +4196,10 @@ } } }, + "loginFlowState": { + "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", + "title": "Login Flow State" + }, "logoutFlow": { "description": "Logout Flow", "type": "object", @@ -4502,6 +4486,10 @@ } } }, + "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" + }, "recoveryIdentityAddress": { "type": "object", "required": [ @@ -4615,6 +4603,10 @@ } } }, + "registrationFlowState": { + "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", + "title": "State represents the state of this request:" + }, "selfServiceFlowExpiredError": { "description": "Is sent when a flow is expired", "type": "object", @@ -4811,6 +4803,10 @@ } } }, + "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:" + }, "successfulCodeExchangeResponse": { "description": "The Response for Registration Flows via API", "type": "object", @@ -5967,6 +5963,10 @@ } } }, + "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" + }, "version": { "type": "object", "properties": { From 343365a082ead4858a9548531dff285c40ddcfc1 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 16 Aug 2023 16:17:08 +0200 Subject: [PATCH 04/24] fix: golangci-lint --- selfservice/strategy/link/strategy_verification.go | 1 - 1 file changed, 1 deletion(-) diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index 804219531d8c..47271cd7191d 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -151,7 +151,6 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio } func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *http.Request, f *verification.Flow) error { - body := new(verificationSubmitPayload) body, err := s.decodeVerification(r) if err != nil { return s.handleVerificationError(w, r, f, body, err) From e61c7485e7eaccfe8934819e0e573ed2b0d0e056 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:08:18 +0200 Subject: [PATCH 05/24] chore: move verification hook to before persist --- driver/registry_default_registration.go | 9 +++--- selfservice/hook/code_address_verifier.go | 28 ++++++------------- .../hook/code_address_verifier_test.go | 20 ++++--------- 3 files changed, 17 insertions(+), 40 deletions(-) diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 060afcbdf5c7..5ea0f7d93345 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -12,6 +12,10 @@ import ( ) func (m *RegistryDefault) PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) (b []registration.PostHookPrePersistExecutor) { + if credentialsType == identity.CredentialsTypeCodeAuth && m.Config().SelfServiceCodeStrategy(ctx).RegistrationEnabled { + b = append(b, m.HookCodeAddressVerifier()) + } + for _, v := range m.getHooks(string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType))) { if hook, ok := v.(registration.PostHookPrePersistExecutor); ok { b = append(b, hook) @@ -28,11 +32,6 @@ 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/selfservice/hook/code_address_verifier.go b/selfservice/hook/code_address_verifier.go index 3c77f3df4ae5..b222970cb9af 100644 --- a/selfservice/hook/code_address_verifier.go +++ b/selfservice/hook/code_address_verifier.go @@ -6,40 +6,27 @@ 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) -) +var _ registration.PostHookPrePersistExecutor = 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 { +func (cv *CodeAddressVerifier) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, i *identity.Identity) error { if a.Active != identity.CredentialsTypeCodeAuth { return nil } @@ -49,14 +36,15 @@ func (cv *CodeAddressVerifier) ExecutePostRegistrationPostPersistHook(w http.Res return err } - for idx := range s.Identity.VerifiableAddresses { - va := s.Identity.VerifiableAddresses[idx] + if recoveryCode == nil { + return nil + } + + for idx := range i.VerifiableAddresses { + va := &i.VerifiableAddresses[idx] if !va.Verified && recoveryCode.Address == va.Value { va.Verified = true va.Status = identity.VerifiableAddressStatusCompleted - if err := cv.r.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &va); err != nil { - return err - } break } } diff --git a/selfservice/hook/code_address_verifier_test.go b/selfservice/hook/code_address_verifier_test.go index b39f59dfbc2e..579432e60556 100644 --- a/selfservice/hook/code_address_verifier_test.go +++ b/selfservice/hook/code_address_verifier_test.go @@ -20,7 +20,6 @@ import ( "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" ) @@ -68,33 +67,24 @@ func TestCodeAddressVerifier(t *testing.T) { 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)) + require.NoError(t, verifier.ExecutePostRegistrationPrePersistHook(nil, r, flow, id)) } t.Run("case=should set the verifiable email address to verified", func(t *testing.T) { address, flow := setup(t) id := setupIdentity(t, address) - + require.False(t, id.VerifiableAddresses[0].Verified) runHook(t, id, flow) - va, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, address) - require.NoError(t, err) - require.True(t, va.Verified) + require.True(t, id.VerifiableAddresses[0].Verified) }) t.Run("case=should ignore verifiable email address that does not match the code", func(t *testing.T) { _, flow := setup(t) newEmail := testhelpers.RandomEmail() id := setupIdentity(t, newEmail) - + require.False(t, id.VerifiableAddresses[0].Verified) runHook(t, id, flow) - va, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, newEmail) - require.NoError(t, err) - require.False(t, va.Verified) + require.False(t, id.VerifiableAddresses[0].Verified) }) } From 5ebe91a245c02ba774ba0be98a068875c9e0b381 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Thu, 17 Aug 2023 11:49:11 +0200 Subject: [PATCH 06/24] chore: refactor code state manager to use generics --- selfservice/strategy/code/strategy.go | 105 +++++++++++------- selfservice/strategy/code/strategy_login.go | 46 ++++---- .../strategy/code/strategy_registration.go | 47 ++++---- 3 files changed, 112 insertions(+), 86 deletions(-) diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index 3438181965ae..032ed792a9e7 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -316,72 +316,101 @@ func (s *Strategy) NewCodeUINodes(r *http.Request, f flow.Flow, data json.RawMes } 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 + FlowType interface { + *login.Flow | *registration.Flow | *recovery.Flow | *verification.Flow + flow.Flow } - CodeStateManagerPayload struct { - Identifier string - Email string - Traits json.RawMessage - TransientPayload json.RawMessage - Resend string - Code string + FlowPayload interface { + *updateLoginFlowWithCodeMethod | *updateRegistrationFlowWithCodeMethod | *updateRecoveryFlowWithCodeMethod | *updateVerificationFlowWithCodeMethod + } + CreateCodeState[T FlowType, P FlowPayload] func(context.Context, T, *Strategy, P) error + ValidateCodeState[T FlowType, P FlowPayload] func(context.Context, T, *Strategy, P) error + AlreadyValidatedCodeState[T FlowType, P FlowPayload] func(context.Context, T, *Strategy, P) error + CodeStateManager[T FlowType, P FlowPayload] struct { + f T + payload P + strategy *Strategy + createCodeState CreateCodeState[T, P] + verifyCodeState ValidateCodeState[T, P] + alreadyValidatedCodeState AlreadyValidatedCodeState[T, P] } ) -func NewCodeStateManager(f flow.Flow, payload *CodeStateManagerPayload) *CodeStateManager { - return &CodeStateManager{ - f: f, - payload: payload, +func NewCodeStateManager[T FlowType, P FlowPayload](f T, s *Strategy, payload P) *CodeStateManager[T, P] { + return &CodeStateManager[T, P]{ + f: f, + strategy: s, + payload: payload, } } -func (c *CodeStateManager) SetCreateCodeHandler(fn CreateCodeState) { +func (c *CodeStateManager[T, P]) SetCreateCodeHandler(fn CreateCodeState[T, P]) { c.createCodeState = fn } -func (c *CodeStateManager) SetCodeVerifyHandler(fn ValidateCodeState) { +func (c *CodeStateManager[T, P]) SetCodeVerifyHandler(fn ValidateCodeState[T, P]) { c.verifyCodeState = fn } -func (c *CodeStateManager) SetCodeDoneHandler(fn AlreadyValidatedCodeState) { +func (c *CodeStateManager[T, P]) SetCodeDoneHandler(fn AlreadyValidatedCodeState[T, P]) { c.alreadyValidatedCodeState = fn } -func (c *CodeStateManager) validatePayload(ctx context.Context) error { - switch c.f.GetFlowName() { - case flow.LoginFlow: - if len(c.payload.Identifier) == 0 { +func (c *CodeStateManager[T, P]) validatePayload(ctx context.Context) error { + switch v := any(c.payload).(type) { + case *updateLoginFlowWithCodeMethod: + if len(v.Identifier) == 0 { return errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) } - case flow.RegistrationFlow: - if len(c.payload.Traits) == 0 { + case *updateRegistrationFlowWithCodeMethod: + if len(v.Traits) == 0 { return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) } - case flow.RecoveryFlow, flow.VerificationFlow: - if len(c.payload.Email) == 0 { + case *updateRecoveryFlowWithCodeMethod: + if len(v.Email) == 0 { + return errors.WithStack(schema.NewRequiredError("#/email", "email")) + } + case *updateVerificationFlowWithCodeMethod: + if len(v.Email) == 0 { return errors.WithStack(schema.NewRequiredError("#/email", "email")) } default: - return errors.New("received unexpected flow type") + return errors.WithStack(herodot.ErrBadRequest.WithReason("received unexpected flow payload type")) } return nil } -func (c *CodeStateManager) Run(ctx context.Context) error { +func (c *CodeStateManager[T, P]) getResend() string { + switch v := any(c.payload).(type) { + case *updateLoginFlowWithCodeMethod: + return v.Resend + case *updateRegistrationFlowWithCodeMethod: + return v.Resend + } + return "" +} + +func (c *CodeStateManager[T, P]) getCode() string { + switch v := any(c.payload).(type) { + case *updateLoginFlowWithCodeMethod: + return v.Code + case *updateRegistrationFlowWithCodeMethod: + return v.Code + case *updateRecoveryFlowWithCodeMethod: + return v.Code + case *updateVerificationFlowWithCodeMethod: + return v.Code + } + return "" +} + +func (c *CodeStateManager[T, P]) 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") { + if strings.EqualFold(c.getResend(), "code") { c.f.SetState(flow.StateChooseMethod) } @@ -393,14 +422,14 @@ func (c *CodeStateManager) Run(ctx context.Context) error { return err } - if err := c.createCodeState(ctx, c.payload); err != nil { + if err := c.createCodeState(ctx, c.f, c.strategy, 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 { + if len(c.getCode()) == 0 { return errors.WithStack(schema.NewRequiredError("#/code", "code")) } @@ -408,11 +437,11 @@ func (c *CodeStateManager) Run(ctx context.Context) error { return err } - if err := c.verifyCodeState(ctx, c.payload); err != nil { + if err := c.verifyCodeState(ctx, c.f, c.strategy, c.payload); err != nil { return err } case flow.StatePassedChallenge: - return c.alreadyValidatedCodeState(ctx, c.payload) + return c.alreadyValidatedCodeState(ctx, c.f, c.strategy, c.payload) default: return errors.WithStack(errors.New("Unknown flow state")) } diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index 9f3ad0f64e2e..e73e9610def4 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -136,25 +136,21 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.HandleLoginError(w, r, f, &p, err) } - codeManager := NewCodeStateManager(f, &CodeStateManagerPayload{ - Identifier: p.Identifier, - Resend: p.Resend, - Code: p.Code, - }) + codeManager := NewCodeStateManager[*login.Flow, *updateLoginFlowWithCodeMethod](f, s, &p) - codeManager.SetCreateCodeHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { - s.deps.Audit(). + codeManager.SetCreateCodeHandler(func(ctx context.Context, f *login.Flow, strategy *Strategy, p *updateLoginFlowWithCodeMethod) error { + strategy.deps.Audit(). WithSensitiveField("identifier", p.Identifier). Info("Creating login code state.") // Step 1: Get the identity - i, cred, err := s.getIdentity(ctx, p.Identifier) + i, cred, err := strategy.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 { + if err := strategy.deps.LoginCodePersister().DeleteLoginCodesOfFlow(ctx, f.GetID()); err != nil { return errors.WithStack(err) } @@ -174,7 +170,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, // 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 { + if err := strategy.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { return errors.WithStack(err) } @@ -195,14 +191,14 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } f.Active = identity.CredentialsTypeCodeAuth - if err = s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + if err = strategy.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { return err } if x.IsJSONRequest(r) { - s.deps.Writer().Write(w, r, f) + strategy.deps.Writer().Write(w, r, f) } else { - http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) + http.Redirect(w, r, f.AppendTo(strategy.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. @@ -210,19 +206,19 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return errors.WithStack(flow.ErrCompletedByStrategy) }) - codeManager.SetCodeVerifyHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { - s.deps.Audit(). + codeManager.SetCodeVerifyHandler(func(ctx context.Context, f *login.Flow, strategy *Strategy, p *updateLoginFlowWithCodeMethod) error { + strategy.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) + i, _, err = strategy.getIdentity(ctx, p.Identifier) if err != nil { return err } - loginCode, err := s.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) + loginCode, err := strategy.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) if err != nil { if errors.Is(err, ErrCodeNotFound) { return schema.NewLoginCodeInvalid() @@ -230,7 +226,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return errors.WithStack(err) } - i, err = s.deps.PrivilegedIdentityPool().GetIdentity(ctx, loginCode.IdentityID, identity.ExpandDefault) + i, err = strategy.deps.PrivilegedIdentityPool().GetIdentity(ctx, loginCode.IdentityID, identity.ExpandDefault) if err != nil { return errors.WithStack(err) } @@ -240,9 +236,9 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, // since nothing has errored yet, we can assume that the code is correct // and we can update the login flow - s.NextFlowState(f) + strategy.NextFlowState(f) - if err := s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + if err := strategy.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { return errors.WithStack(err) } @@ -251,7 +247,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, 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 { + if err := strategy.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &va); err != nil { return err } break @@ -261,11 +257,11 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil }) - codeManager.SetCodeDoneHandler(func(ctx context.Context, codePayload *CodeStateManagerPayload) error { - s.deps.Audit(). - WithSensitiveField("identifier", codePayload.Identifier). + codeManager.SetCodeDoneHandler(func(ctx context.Context, f *login.Flow, strategy *Strategy, p *updateLoginFlowWithCodeMethod) error { + strategy.deps.Audit(). + WithSensitiveField("identifier", p.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())) + return errors.WithStack(schema.NewNoLoginStrategyResponsible()) }) if err := codeManager.Run(r.Context()); err != nil { diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index 96cc224a8c3c..896331578865 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -143,15 +143,10 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat 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 := NewCodeStateManager[*registration.Flow, *updateRegistrationFlowWithCodeMethod](f, s, &p) - codeManager.SetCreateCodeHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { - s.deps.Logger(). + codeManager.SetCreateCodeHandler(func(ctx context.Context, f *registration.Flow, strategy *Strategy, p *updateRegistrationFlowWithCodeMethod) error { + strategy.deps.Logger(). WithSensitiveField("traits", p.Traits). WithSensitiveField("transient_paylaod", p.TransientPayload). Debug("Creating registration code.") @@ -159,13 +154,13 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat // Create the Registration code // Step 1: validate the identity's traits - cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + cred, err := strategy.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 { + if err := strategy.deps.RegistrationCodePersister().DeleteRegistrationCodesOfFlow(ctx, f.ID); err != nil { return errors.WithStack(err) } @@ -176,27 +171,27 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } // 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 { + if err := strategy.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { return errors.WithStack(err) } // sets the flow state to code sent - s.NextFlowState(f) + strategy.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 { + if err := strategy.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 { + if err := strategy.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { return errors.WithStack(err) } if x.IsJSONRequest(r) { - s.deps.Writer().Write(w, r, f) + strategy.deps.Writer().Write(w, r, f) } else { http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther) } @@ -206,8 +201,8 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return errors.WithStack(flow.ErrCompletedByStrategy) }) - codeManager.SetCodeVerifyHandler(func(ctx context.Context, p *CodeStateManagerPayload) error { - s.deps.Logger(). + codeManager.SetCodeVerifyHandler(func(ctx context.Context, f *registration.Flow, strategy *Strategy, p *updateRegistrationFlowWithCodeMethod) error { + strategy.deps.Logger(). WithSensitiveField("traits", p.Traits). WithSensitiveField("transient_payload", p.TransientPayload). WithSensitiveField("code", p.Code). @@ -216,7 +211,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat // 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) + cred, err := strategy.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) if err != nil { return err } @@ -229,7 +224,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } // Step 3: Attempt to use the code - registrationCode, err := s.deps.RegistrationCodePersister().UseRegistrationCode(ctx, f.ID, p.Code, cred.Identifiers...) + registrationCode, err := strategy.deps.RegistrationCodePersister().UseRegistrationCode(ctx, f.ID, p.Code, cred.Identifiers...) if err != nil { if errors.Is(err, ErrCodeNotFound) { return errors.WithStack(schema.NewRegistrationCodeInvalid()) @@ -238,22 +233,28 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } // 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 { + if err := strategy.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) + strategy.NextFlowState(f) - if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + if err := strategy.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { return errors.WithStack(err) } return nil }) - codeManager.SetCodeDoneHandler(func(ctx context.Context, _ *CodeStateManagerPayload) error { + codeManager.SetCodeDoneHandler(func(ctx context.Context, f *registration.Flow, strategy *Strategy, p *updateRegistrationFlowWithCodeMethod) error { + strategy.deps.Audit(). + WithSensitiveField("traits", p.Traits). + WithSensitiveField("transient_payload", p.TransientPayload). + WithSensitiveField("code", p.Code). + Debug("The registration flow has already been completed, but is being re-requested.") + return errors.WithStack(schema.NewNoRegistrationStrategyResponsible()) }) From 2107a7e038c2ea2a1b6456ddba6624d2488f6085 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:00:11 +0200 Subject: [PATCH 07/24] test: registration with spa and browser --- .../code/strategy_registration_test.go | 581 ++++++++++++------ 1 file changed, 376 insertions(+), 205 deletions(-) diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index d070464a8cf7..df4c90a58897 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -5,6 +5,7 @@ package code_test import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -25,10 +26,10 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" "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 { @@ -36,6 +37,8 @@ type state struct { csrfToken string client *http.Client email string + + registrationFlow *kratos.RegistrationFlow } func TestRegistrationCodeStrategyDisabled(t *testing.T) { @@ -102,102 +105,82 @@ func TestRegistrationCodeStrategy(t *testing.T) { return conf, reg, public } - createRegistrationFlow := func(ctx context.Context, t *testing.T, publicURL string) *state { + createRegistrationFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, isSPA bool) *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) + clientInit := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, public, isSPA, false, false) - body, err := io.ReadAll(resp.Body) + body, err := json.Marshal(clientInit) 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, + csrfToken: csrfToken, + client: client, + flowID: clientInit.GetId(), + registrationFlow: clientInit, } } - type onSubmitAssertion func(ctx context.Context, t *testing.T, s *state, resp *http.Response) + type onSubmitAssertion func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) - registerNewUser := func(ctx context.Context, t *testing.T, publicURL string, s *state, submitAssertion onSubmitAssertion) *state { + registerNewUser := func(ctx context.Context, t *testing.T, s *state, isSPA bool, 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") + values := testhelpers.SDKFormFieldsToURLValues(s.registrationFlow.Ui.Nodes) + values.Set("traits.email", email) + values.Set("csrf_token", s.csrfToken) + values.Set("method", "code") - client := s.client + // TODO: API client + body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, s.registrationFlow, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) // 2. Submit Identifier (email) - resp, err := client.Do(req) - require.NoError(t, err) if submitAssertion != nil { - submitAssertion(ctx, t, s, resp) + submitAssertion(ctx, t, s, body, 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() + require.Equal(t, http.StatusOK, resp.StatusCode) + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() assert.NotEmptyf(t, csrfToken, "%s", body) - require.Equal(t, email, gjson.GetBytes(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + require.Equal(t, email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) } - require.NoError(t, resp.Body.Close()) + var rf *kratos.RegistrationFlow + require.NoError(t, json.Unmarshal([]byte(body), &rf)) + s.registrationFlow = rf return s } - submitOTP := func(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, publicURL string, s *state, otp string, shouldHaveSessionCookie bool, submitAssertion onSubmitAssertion) *state { + submitOTP := func(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, s *state, otp string, isSPA 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) + values := testhelpers.SDKFormFieldsToURLValues(s.registrationFlow.Ui.Nodes) + values.Set("csrf_token", s.csrfToken) + values.Set("method", "code") + values.Set("code", otp) + values.Set("traits.email", s.email) - 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) + // TODO: API clients + body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, s.registrationFlow, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) if submitAssertion != nil { - submitAssertion(ctx, t, s, resp) + submitAssertion(ctx, t, s, body, resp) return s } + require.Equal(t, http.StatusOK, resp.StatusCode) + verifiableAddress, err := reg.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, s.email) require.NoError(t, err) require.Equal(t, s.email, verifiableAddress.Value) @@ -209,18 +192,6 @@ func TestRegistrationCodeStrategy(t *testing.T) { _, 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 } @@ -230,142 +201,273 @@ func TestRegistrationCodeStrategy(t *testing.T) { 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() + t.Run("test=SPA client", func(t *testing.T) { + t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { + ctx := context.Background() - // 1. Initiate flow - state := createRegistrationFlow(ctx, t, public.URL) + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, true) - // 2. Submit Identifier (email) - state = registerNewUser(ctx, t, public.URL, state, nil) + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, true, 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") + 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) + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) - // 3. Submit OTP - state = submitOTP(ctx, t, reg, public.URL, state, registrationCode, true, nil) - }) + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, registrationCode, true, nil) + }) - t.Run("case=should be able to resend the code", func(t *testing.T) { - ctx := context.Background() + t.Run("case=should be able to resend the code", func(t *testing.T) { + ctx := context.Background() - s := createRegistrationFlow(ctx, t, public.URL) + s := createRegistrationFlow(ctx, t, public, true) - 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) + s = registerNewUser(ctx, t, s, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusOK, resp.StatusCode) + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + + attr := gjson.Get(body, "ui.nodes.#(attributes.name==method)#").String() + require.NotEmpty(t, attr) + + val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() + require.Equal(t, "code", val) + }) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // resend code + 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) - 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) + 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") - val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() - require.Equal(t, "code", val) + registrationCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode2) + + require.NotEqual(t, registrationCode, registrationCode2) + + // try submit old code + s = submitOTP(ctx, t, reg, s, registrationCode, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") + }) + + s = submitOTP(ctx, t, reg, s, registrationCode2, true, 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") + t.Run("case=swapping out traits should not be possible on code submit", func(t *testing.T) { + ctx := context.Background() - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, true) - // 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) + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, true, nil) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") + 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") - resp, err := s.client.Do(req) - require.NoError(t, err) + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) - require.Equal(t, http.StatusOK, resp.StatusCode) + s.email = "not-" + s.email // swap out email - // 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") + // 3. Submit OTP + s = submitOTP(ctx, t, reg, s, registrationCode, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + }) + }) - registrationCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode2) + t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { + ctx := context.Background() - require.NotEqual(t, registrationCode, registrationCode2) + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, true) - // 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") + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, true, nil) + + reg.Persister().Transaction(ctx, func(ctx context.Context, connection *pop.Connection) error { + count, err := connection.RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ?", new(code.RegistrationCode).TableName(ctx)), uuid.FromStringOrNil(s.flowID)).Count(new(code.RegistrationCode)) + require.NoError(t, err) + require.Equal(t, 1, count) + return nil + }) + + for i := 0; i < 5; i++ { + s = submitOTP(ctx, t, reg, s, "111111", true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + }) + } + + s = submitOTP(ctx, t, reg, s, "111111", true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) }) - 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() + t.Run("test=Browser client", func(t *testing.T) { + t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { + ctx := context.Background() - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public.URL) + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, false) - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, public.URL, s, nil) + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, false, 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") + 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) + 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.") + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, registrationCode, false, nil) }) - }) - t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { - ctx := context.Background() + t.Run("case=should be able to resend the code", func(t *testing.T) { + ctx := context.Background() + + s := createRegistrationFlow(ctx, t, public, false) + + s = registerNewUser(ctx, t, s, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusOK, resp.StatusCode) + + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public.URL) + attr := gjson.Get(body, "ui.nodes.#(attributes.name==method)#").String() + require.NotEmpty(t, attr) - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, public.URL, s, nil) + 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") - 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)) + 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, 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.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, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, 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") + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") }) - } - s = submitOTP(ctx, t, reg, 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.") + s = submitOTP(ctx, t, reg, s, registrationCode2, false, 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, false) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s.email = "not-" + s.email // swap out email + + // 3. Submit OTP + s = submitOTP(ctx, t, reg, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusOK, resp.Status) + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + }) + }) + + t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, false) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, false, nil) + + reg.Persister().Transaction(ctx, func(ctx context.Context, connection *pop.Connection) error { + count, err := connection.RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ?", new(code.RegistrationCode).TableName(ctx)), uuid.FromStringOrNil(s.flowID)).Count(new(code.RegistrationCode)) + require.NoError(t, err) + require.Equal(t, 1, count) + return nil + }) + + for i := 0; i < 5; i++ { + s = submitOTP(ctx, t, reg, s, "111111", false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + }) + } + + s = submitOTP(ctx, t, reg, s, "111111", false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) + }) }) @@ -373,73 +475,142 @@ func TestRegistrationCodeStrategy(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") + t.Run("test=SPA client", func(t *testing.T) { + t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, true) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages").String(), "Could not find any login identifiers") + }) }) - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public.URL) + 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{}{}) - // 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.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) + }) + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, true) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, true, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, registrationCode, true, 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, true) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, true, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s = submitOTP(ctx, t, reg, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusGone, resp.StatusCode) + require.Contains(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") + }) + }) + }) - 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.Run("test=Browser client", func(t *testing.T) { + t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + }) + + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, false) - t.Cleanup(func() { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ - {"hook": "session"}, + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages").String(), "Could not find any login identifiers") }) }) - // 1. Initiate flow - state := createRegistrationFlow(ctx, t, public.URL) + 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{}{}) - // 2. Submit Identifier (email) - state = registerNewUser(ctx, t, public.URL, state, nil) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) + }) - 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") + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, false) - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, false, nil) - // 3. Submit OTP - state = submitOTP(ctx, t, reg, public.URL, state, registrationCode, false, 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") - 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") + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, registrationCode, false, nil) }) - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public.URL) + 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") + }) - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, public.URL, s, nil) + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, false) - 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") + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, false, nil) - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) + 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") - 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") + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s = submitOTP(ctx, t, reg, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusGone, resp.StatusCode) + require.Contains(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") + }) }) }) + }) } From 2f45939ae3a06db6ebd6b68c130ac7890b4d2d21 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Fri, 18 Aug 2023 14:01:10 +0200 Subject: [PATCH 08/24] test: registration with code on browser and spa --- .schema/openapi/patches/identity.yaml | 3 +- .../model_identity_credentials_type.go | 3 +- .../model_identity_credentials_type.go | 3 +- .../code/strategy_registration_test.go | 651 +++++++----------- spec/api.json | 6 +- 5 files changed, 269 insertions(+), 397 deletions(-) diff --git a/.schema/openapi/patches/identity.yaml b/.schema/openapi/patches/identity.yaml index a227523488cf..c5f5ede63c58 100644 --- a/.schema/openapi/patches/identity.yaml +++ b/.schema/openapi/patches/identity.yaml @@ -11,6 +11,7 @@ - oidc - webauthn - lookup_secret + - code - op: add path: /paths/~1admin~1identities~1{id}/get/parameters/1/schema/items/enum value: @@ -19,6 +20,7 @@ - oidc - webauthn - lookup_secret + - code - op: remove path: /components/schemas/updateIdentityBody/properties/metadata_admin/type - op: remove @@ -32,4 +34,3 @@ - op: add path: /components/schemas/nullJsonRawMessage/nullable value: true - diff --git a/internal/client-go/model_identity_credentials_type.go b/internal/client-go/model_identity_credentials_type.go index f02f3f19b898..a9d01c54e796 100644 --- a/internal/client-go/model_identity_credentials_type.go +++ b/internal/client-go/model_identity_credentials_type.go @@ -26,6 +26,7 @@ const ( IDENTITYCREDENTIALSTYPE_OIDC IdentityCredentialsType = "oidc" IDENTITYCREDENTIALSTYPE_WEBAUTHN IdentityCredentialsType = "webauthn" IDENTITYCREDENTIALSTYPE_LOOKUP_SECRET IdentityCredentialsType = "lookup_secret" + IDENTITYCREDENTIALSTYPE_CODE IdentityCredentialsType = "code" ) func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { @@ -35,7 +36,7 @@ func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { return err } enumTypeValue := IdentityCredentialsType(value) - for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret"} { + for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret", "code"} { if existing == enumTypeValue { *v = enumTypeValue return nil diff --git a/internal/httpclient/model_identity_credentials_type.go b/internal/httpclient/model_identity_credentials_type.go index f02f3f19b898..a9d01c54e796 100644 --- a/internal/httpclient/model_identity_credentials_type.go +++ b/internal/httpclient/model_identity_credentials_type.go @@ -26,6 +26,7 @@ const ( IDENTITYCREDENTIALSTYPE_OIDC IdentityCredentialsType = "oidc" IDENTITYCREDENTIALSTYPE_WEBAUTHN IdentityCredentialsType = "webauthn" IDENTITYCREDENTIALSTYPE_LOOKUP_SECRET IdentityCredentialsType = "lookup_secret" + IDENTITYCREDENTIALSTYPE_CODE IdentityCredentialsType = "code" ) func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { @@ -35,7 +36,7 @@ func (v *IdentityCredentialsType) UnmarshalJSON(src []byte) error { return err } enumTypeValue := IdentityCredentialsType(value) - for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret"} { + for _, existing := range []IdentityCredentialsType{"password", "totp", "oidc", "webauthn", "lookup_secret", "code"} { if existing == enumTypeValue { *v = enumTypeValue return nil diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index df4c90a58897..b3fd9d99c905 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -26,19 +26,16 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" - kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/strategy/code" ) type state struct { - flowID string - csrfToken string - client *http.Client - email string - - registrationFlow *kratos.RegistrationFlow + flowID string + client *http.Client + email string + testServer *httptest.Server } func TestRegistrationCodeStrategyDisabled(t *testing.T) { @@ -109,6 +106,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { t.Helper() client := testhelpers.NewClientWithCookies(t) + client.Transport = testhelpers.NewTransportWithLogger(http.DefaultTransport, t).RoundTripper clientInit := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, public, isSPA, false, false) body, err := json.Marshal(clientInit) @@ -121,10 +119,9 @@ func TestRegistrationCodeStrategy(t *testing.T) { require.Truef(t, gjson.GetBytes(body, "ui.nodes.#(attributes.value==code)").Exists(), "%s", body) return &state{ - csrfToken: csrfToken, - client: client, - flowID: clientInit.GetId(), - registrationFlow: clientInit, + client: client, + flowID: clientInit.GetId(), + testServer: public, } } @@ -134,45 +131,48 @@ func TestRegistrationCodeStrategy(t *testing.T) { t.Helper() email := testhelpers.RandomEmail() - s.email = email - values := testhelpers.SDKFormFieldsToURLValues(s.registrationFlow.Ui.Nodes) + rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + values := testhelpers.SDKFormFieldsToURLValues(rf.Ui.Nodes) values.Set("traits.email", email) - values.Set("csrf_token", s.csrfToken) values.Set("method", "code") - // TODO: API client - body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, s.registrationFlow, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) + body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) - // 2. Submit Identifier (email) if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) - } else { - require.Equal(t, http.StatusOK, resp.StatusCode) - csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() - assert.NotEmptyf(t, csrfToken, "%s", body) - require.Equal(t, email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + return s } - var rf *kratos.RegistrationFlow - require.NoError(t, json.Unmarshal([]byte(body), &rf)) - s.registrationFlow = rf + require.Equal(t, http.StatusOK, resp.StatusCode) + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + assert.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) return s } - submitOTP := func(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, s *state, otp string, isSPA bool, submitAssertion onSubmitAssertion) *state { + submitOTP := func(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, s *state, vals func(v *url.Values), isSPA bool, submitAssertion onSubmitAssertion) *state { t.Helper() - values := testhelpers.SDKFormFieldsToURLValues(s.registrationFlow.Ui.Nodes) - values.Set("csrf_token", s.csrfToken) - values.Set("method", "code") - values.Set("code", otp) + rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + values := testhelpers.SDKFormFieldsToURLValues(rf.Ui.Nodes) + // the sdk to values always adds resend which isn't what we always need here. + // so we delete it here. + // the custom vals func can add it again if needed. + values.Del("resend") + values.Set("traits.email", s.email) + vals(&values) - // TODO: API clients - body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, s.registrationFlow, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) + body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) @@ -201,416 +201,283 @@ func TestRegistrationCodeStrategy(t *testing.T) { ctx := context.Background() _, reg, public := setup(ctx, t) - t.Run("test=SPA client", func(t *testing.T) { - t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { - ctx := context.Background() - - // 1. Initiate flow - state := createRegistrationFlow(ctx, t, public, true) - - // 2. Submit Identifier (email) - state = registerNewUser(ctx, t, state, true, nil) - - message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") - assert.Contains(t, message.Body, "please complete your account registration by entering the following code") - - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) - - // 3. Submit OTP - state = submitOTP(ctx, t, reg, state, 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, true) - - s = registerNewUser(ctx, t, s, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusOK, resp.StatusCode) - csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() - require.NotEmptyf(t, csrfToken, "%s", body) - require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) - - attr := gjson.Get(body, "ui.nodes.#(attributes.name==method)#").String() - require.NotEmpty(t, attr) - - val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() - require.Equal(t, "code", val) + for _, tc := range []struct { + d string + isSPA bool + }{ + { + d: "SPA client", + isSPA: true, + }, + { + d: "Browser client", + isSPA: false, + }, + } { + t.Run("flow="+tc.d, func(t *testing.T) { + t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, nil) }) - 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") + t.Run("case=should be able to resend the code", func(t *testing.T) { + ctx := context.Background() - resp, err := s.client.Do(req) - require.NoError(t, err) + s := createRegistrationFlow(ctx, t, public, tc.isSPA) - 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, s, registrationCode, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") - }) - - s = submitOTP(ctx, t, reg, s, registrationCode2, true, nil) - }) + s = registerNewUser(ctx, t, s, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusOK, resp.StatusCode) + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmptyf(t, csrfToken, "%s", body) + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) - t.Run("case=swapping out traits should not be possible on code submit", func(t *testing.T) { - ctx := context.Background() + attr := gjson.Get(body, "ui.nodes.#(attributes.name==method)#").String() + require.NotEmpty(t, attr) - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public, true) - - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, true, nil) - - message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") - assert.Contains(t, message.Body, "please complete your account registration by entering the following code") - - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) - - s.email = "not-" + s.email // swap out email - - // 3. Submit OTP - s = submitOTP(ctx, t, reg, s, registrationCode, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") - }) - }) - - t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { - ctx := context.Background() - - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public, true) - - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, true, nil) - - reg.Persister().Transaction(ctx, func(ctx context.Context, connection *pop.Connection) error { - count, err := connection.RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ?", new(code.RegistrationCode).TableName(ctx)), uuid.FromStringOrNil(s.flowID)).Count(new(code.RegistrationCode)) - require.NoError(t, err) - require.Equal(t, 1, count) - return nil - }) - - for i := 0; i < 5; i++ { - s = submitOTP(ctx, t, reg, s, "111111", true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() + require.Equal(t, "code", val) }) - } - - s = submitOTP(ctx, t, reg, s, "111111", true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") - }) - }) - - }) - - t.Run("test=Browser client", func(t *testing.T) { - t.Run("case=should be able to register with code identity credentials", func(t *testing.T) { - ctx := context.Background() - // 1. Initiate flow - state := createRegistrationFlow(ctx, t, public, false) + 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") - // 2. Submit Identifier (email) - state = registerNewUser(ctx, t, state, false, nil) - - message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") - assert.Contains(t, message.Body, "please complete your account registration by entering the following code") - - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) - - // 3. Submit OTP - state = submitOTP(ctx, t, reg, state, registrationCode, false, nil) - }) - - t.Run("case=should be able to resend the code", func(t *testing.T) { - ctx := context.Background() - - s := createRegistrationFlow(ctx, t, public, false) - - s = registerNewUser(ctx, t, s, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusOK, resp.StatusCode) - - csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() - require.NotEmptyf(t, csrfToken, "%s", body) - require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) - - attr := gjson.Get(body, "ui.nodes.#(attributes.name==method)#").String() - require.NotEmpty(t, attr) - - val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() - require.Equal(t, "code", val) - }) + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) - 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) + // resend code + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("resend", "code") + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + require.Equal(t, http.StatusOK, resp.StatusCode) + csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() + require.NotEmptyf(t, csrfToken, "%s", body) + require.Containsf(t, gjson.Get(body, "ui.messages").String(), "An email containing a code has been sent to the email address you provided.", "%s", body) + }) - require.NotEqual(t, registrationCode, registrationCode2) + // get the new code from email + message = testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode2) + + require.NotEqual(t, registrationCode, registrationCode2) + + // try submit old code + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") + }) - // try submit old code - s = submitOTP(ctx, t, reg, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages").String(), "The registration code is invalid or has already been used. Please try again") + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode2) + }, tc.isSPA, nil) }) - s = submitOTP(ctx, t, reg, s, registrationCode2, false, nil) - }) - - t.Run("case=swapping out traits should not be possible on code submit", func(t *testing.T) { - ctx := context.Background() + 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, false) + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, false, nil) + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, nil) - message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") - assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + 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) + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) - s.email = "not-" + s.email // swap out email + s.email = "not-" + s.email // swap out email - // 3. Submit OTP - s = submitOTP(ctx, t, reg, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusOK, resp.Status) - require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + // 3. Submit OTP + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The provided traits do not match the traits previously associated with this flow.") + }) }) - }) - - t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { - ctx := context.Background() - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public, false) + t.Run("case=code should not be able to use more than 5 times", func(t *testing.T) { + ctx := context.Background() - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, false, nil) + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) - 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 - }) + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, nil) - for i := 0; i < 5; i++ { - s = submitOTP(ctx, t, reg, s, "111111", false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + 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 }) - } - s = submitOTP(ctx, t, reg, s, "111111", false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + for i := 0; i < 5; i++ { + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", "111111") + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The registration code is invalid or has already been used") + }) + } + + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", "111111") + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode, "%s", body) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + require.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) }) }) - - }) + } }) t.Run("test=cases with different configs", func(t *testing.T) { ctx := context.Background() conf, reg, public := setup(ctx, t) - t.Run("test=SPA client", func(t *testing.T) { - t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") - t.Cleanup(func() { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") - }) - - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public, true) - - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, true, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(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"}, + for _, tc := range []struct { + d string + isSPA bool + }{ + { + d: "SPA client", + isSPA: true, + }, + { + d: "Browser client", + isSPA: false, + }, + } { + t.Run("test="+tc.d, func(t *testing.T) { + t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") }) - }) - - // 1. Initiate flow - state := createRegistrationFlow(ctx, t, public, true) - - // 2. Submit Identifier (email) - state = registerNewUser(ctx, t, state, true, nil) - - message := testhelpers.CourierExpectMessage(ctx, t, reg, state.email, "Complete your account registration") - assert.Contains(t, message.Body, "please complete your account registration by entering the following code") - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) - - // 3. Submit OTP - state = submitOTP(ctx, t, reg, state, registrationCode, true, 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, true) - - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, true, nil) - - message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") - assert.Contains(t, message.Body, "please complete your account registration by entering the following code") - - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) - - s = submitOTP(ctx, t, reg, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusGone, resp.StatusCode) - require.Contains(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") - }) - }) - - }) - - t.Run("test=Browser client", func(t *testing.T) { - t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") - t.Cleanup(func() { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") - }) - - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public, false) - - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Contains(t, gjson.Get(body, "ui.messages").String(), "Could not find any login identifiers") + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, gjson.Get(body, "ui.messages").String(), "Could not find any login identifiers") + } else { + // we expect a redirect to the registration page with the flow id + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, conf.SelfServiceFlowRegistrationUI(ctx).Path, resp.Request.URL.Path) + rf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetRegistrationFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(rf) + require.NoError(t, err) + require.Contains(t, gjson.GetBytes(body, "ui.messages").String(), "Could not find any login identifiers") + + } + }) }) - }) - t.Run("case=should have verifiable address even if after session hook is disabled", func(t *testing.T) { - // disable the after session hook - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{}) + t.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"}, + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) }) - }) - - // 1. Initiate flow - state := createRegistrationFlow(ctx, t, public, false) - // 2. Submit Identifier (email) - state = registerNewUser(ctx, t, state, false, nil) + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.isSPA) - 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") + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.isSPA, nil) - registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, registrationCode) + 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") - // 3. Submit OTP - state = submitOTP(ctx, t, reg, state, registrationCode, false, nil) - }) + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) - 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") + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, nil) }) - // 1. Initiate flow - s := createRegistrationFlow(ctx, t, public, false) - - // 2. Submit Identifier (email) - s = registerNewUser(ctx, t, s, false, 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) + 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") + }) - s = submitOTP(ctx, t, reg, s, registrationCode, false, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusGone, resp.StatusCode) - require.Contains(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") + // 1. Initiate flow + s := createRegistrationFlow(ctx, t, public, tc.isSPA) + + // 2. Submit Identifier (email) + s = registerNewUser(ctx, t, s, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + s = submitOTP(ctx, t, reg, s, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.Equal(t, http.StatusGone, resp.StatusCode) + require.Containsf(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago", "%s", body) + } else { + // with browser clients we redirect back to the UI with a new flow id as a query parameter + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, conf.SelfServiceFlowRegistrationUI(ctx).Path, resp.Request.URL.Path) + require.NotEqual(t, s.flowID, resp.Request.URL.Query().Get("flow")) + } + }) }) }) - }) - + } }) } diff --git a/spec/api.json b/spec/api.json index 4e6c2bbeda7d..e5f2261f347f 100644 --- a/spec/api.json +++ b/spec/api.json @@ -971,7 +971,8 @@ "totp", "oidc", "webauthn", - "lookup_secret" + "lookup_secret", + "code" ], "title": "CredentialsType represents several different credential types, like password credentials, passwordless credentials,", "type": "string" @@ -3653,7 +3654,8 @@ "totp", "oidc", "webauthn", - "lookup_secret" + "lookup_secret", + "code" ], "type": "string" }, From 361b316b2f4e0cd9dd19a5d6ffd67489fb39d1ce Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:59:30 +0200 Subject: [PATCH 09/24] test: login with code on spa and browser clients --- .../strategy/code/strategy_login_test.go | 502 ++++++++++-------- .../code/strategy_registration_test.go | 1 - 2 files changed, 288 insertions(+), 215 deletions(-) diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index 9e8035e09969..2f9310e77d6b 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -5,11 +5,11 @@ package code_test import ( "context" + "encoding/json" "fmt" - "io" "net/http" + "net/http/httptest" "net/url" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -20,8 +20,7 @@ import ( "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/kratos/session" "github.com/ory/x/sqlxx" ) @@ -39,7 +38,7 @@ func TestLoginCodeStrategy(t *testing.T) { public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) - createIdentity := func(t *testing.T, moreIdentifiers ...string) *identity.Identity { + createIdentity := func(ctx context.Context, t *testing.T, moreIdentifiers ...string) *identity.Identity { t.Helper() i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) email := testhelpers.RandomEmail() @@ -74,280 +73,355 @@ func TestLoginCodeStrategy(t *testing.T) { type state struct { flowID string - csrfToken string identity *identity.Identity client *http.Client loginCode string identityEmail string + testServer *httptest.Server } - createLoginFlow := func(t *testing.T, moreIdentifiers ...string) *state { + createLoginFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, isSPA bool, moreIdentifiers ...string) *state { t.Helper() - identity := createIdentity(t, moreIdentifiers...) - client := testhelpers.NewClientWithCookies(t) + identity := createIdentity(ctx, t, moreIdentifiers...) - // 1. Initiate flow - resp, err := client.Get(public.URL + login.RouteInitBrowserFlow) - require.NoError(t, err) + client := testhelpers.NewClientWithCookies(t) + client.Transport = testhelpers.NewTransportWithLogger(http.DefaultTransport, t).RoundTripper + clientInit := testhelpers.InitializeLoginFlowViaBrowser(t, client, public, false, isSPA, false, false) - body, err := io.ReadAll(resp.Body) + body, err := json.Marshal(clientInit) 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, + flowID: clientInit.GetId(), identity: identity, identityEmail: loginEmail, client: client, + testServer: public, } } - type onSubmitAssertion func(t *testing.T, s *state, res *http.Response) + type onSubmitAssertion func(t *testing.T, s *state, body string, res *http.Response) - submitLoginID := func(t *testing.T, s *state, submitAssertion onSubmitAssertion) *state { + submitLogin := func(ctx context.Context, t *testing.T, s *state, isSPA bool, vals func(v *url.Values), mustHaveSession bool, 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) + lf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") + values := testhelpers.SDKFormFieldsToURLValues(lf.Ui.Nodes) + // we need to remove resend here + // since it is not required for the first request + // subsequent requests might need it later + values.Del("resend") + values.Set("method", "code") + vals(&values) - resp, err := s.client.Do(req) - require.NoError(t, err) + body, resp := testhelpers.LoginMakeRequest(t, false, isSPA, lf, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) if submitAssertion != nil { - submitAssertion(t, s, resp) + submitAssertion(t, s, body, 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 + if mustHaveSession { + resp, err = s.client.Get(s.testServer.URL + session.RouteWhoami) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) } - 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) + for _, tc := range []struct { + d string + isSPA bool + }{ + { + d: "SPA client", + isSPA: true, + }, + { + d: "Browser client", + isSPA: false, + }, + } { + + t.Run("test="+tc.d, func(t *testing.T) { + + t.Run("case=should be able to log in with code", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + }) - 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") + t.Run("case=should not be able to change submitted id on code submit", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", "not-"+s.identityEmail) + v.Set("code", loginCode) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + require.EqualValues(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) + + lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } + }) + }) - loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, loginCode) + t.Run("case=should not be able to proceed to code entry when the account is unknown", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", testhelpers.RandomEmail()) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + require.EqualValues(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) + + lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") + } + + }) + }) - s.loginCode = loginCode + t.Run("case=should not be able to use valid code after 5 attempts", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + for i := 0; i < 5; i++ { + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", "111111") + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + // in browser flows we redirect back to the login ui + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The login code is invalid or has already been used") + }) + } + + // 3. Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + // in browser flows we redirect back to the login ui + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "The request was submitted too often.") + }) + }) - // 3. Submit OTP - submitLoginCode(t, s, nil) - }) + 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(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, resp *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusGone, resp.StatusCode) + require.Contains(t, gjson.Get(body, "error.reason").String(), "self-service flow expired 0.00 minutes ago") + } else { + // with browser clients we redirect back to the UI with a new flow id as a query parameter + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) + require.NotEqual(t, s.flowID, resp.Request.URL.Query().Get("flow")) + } + }) + }) - t.Run("case=should not be able to change submitted id on code submit", func(t *testing.T) { - // create login flow - s := createLoginFlow(t) + t.Run("case=resend code shoud invalidate previous code", func(t *testing.T) { + ctx := context.Background() + + s := createLoginFlow(ctx, t, public, tc.isSPA) + + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // resend code + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("resend", "code") + v.Set("identifier", s.identityEmail) + }, false, nil) + + message = testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode2 := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode2) + + assert.NotEqual(t, loginCode, loginCode2) + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, false, func(t *testing.T, s *state, body string, res *http.Response) { + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, res.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, res.StatusCode) + } + require.Contains(t, gjson.Get(body, "ui.messages").String(), "The login code is invalid or has already been used. Please try again") + }) + + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode2) + v.Set("identifier", s.identityEmail) + }, true, nil) + }) - // submit email - s = submitLoginID(t, s, nil) + t.Run("case=on login with un-verified address, should verify it", func(t *testing.T) { + s := createLoginFlow(ctx, t, public, tc.isSPA, testhelpers.RandomEmail()) - 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") + // we need to fetch only the first email + loginEmail := gjson.Get(s.identity.Traits.String(), "email_1").String() + require.NotEmpty(t, loginEmail) - loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, loginCode) + s.identityEmail = loginEmail - s.loginCode = loginCode - s.identityEmail = "not-" + s.identityEmail + var va *identity.VerifiableAddress - // 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") - }) - }) + for _, v := range s.identity.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } - t.Run("case=should not be able to proceed to code entry when the account is unknown", func(t *testing.T) { - s := createLoginFlow(t) + require.NotNil(t, va) + require.False(t, va.Verified) - s.identityEmail = testhelpers.RandomEmail() + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) - // 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") - }) - }) + 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") - t.Run("case=should not be able to use valid code after 5 attempts", func(t *testing.T) { - s := createLoginFlow(t) + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + require.NotEmpty(t, loginCode) - // submit email - s = submitLoginID(t, s, nil) + // Submit OTP + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + v.Set("identifier", s.identityEmail) + }, true, 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) + id, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, s.identity.ID, identity.ExpandEverything) + require.NoError(t, err) - for i := 0; i < 5; i++ { + va = nil - s.loginCode = "111111" + for _, v := range id.VerifiableAddresses { + if v.Value == loginEmail { + va = &v + break + } + } - // 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") + require.NotNil(t, va) + require.True(t, va.Verified) }) - } - - 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_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index b3fd9d99c905..f88af0d0ef80 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -168,7 +168,6 @@ func TestRegistrationCodeStrategy(t *testing.T) { // so we delete it here. // the custom vals func can add it again if needed. values.Del("resend") - values.Set("traits.email", s.email) vals(&values) From 11183d585d3f55fa0ed302b16b8a81ebcf434fab Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:53:44 +0200 Subject: [PATCH 10/24] test(e2e): login and registration with code errors --- persistence/sql/persister_login.go | 2 +- .../strategy/code/strategy_login_test.go | 4 +- .../strategy/code/strategy_registration.go | 5 + .../profiles/code/login/error.spec.ts | 275 +++++++++--------- .../profiles/code/registration/error.spec.ts | 233 ++++++++------- .../code/registration/success.spec.ts | 4 - test/e2e/cypress/support/commands.ts | 61 ++-- test/e2e/profiles/code/.kratos.yml | 9 +- 8 files changed, 302 insertions(+), 291 deletions(-) diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go index 2982216abec5..d34a832167f5 100644 --- a/persistence/sql/persister_login.go +++ b/persistence/sql/persister_login.go @@ -100,7 +100,7 @@ func (p *Persister) CreateLoginCode(ctx context.Context, codeParams *code.Create AddressType: codeParams.AddressType, CodeHMAC: p.hmacValue(ctx, codeParams.RawCode), IssuedAt: now, - ExpiresAt: now.Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), FlowID: codeParams.FlowID, NID: p.NetworkID(ctx), ID: uuid.Nil, diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index 2f9310e77d6b..b29cbac41647 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -288,7 +288,7 @@ func TestLoginCodeStrategy(t *testing.T) { t.Run("case=code should expire", func(t *testing.T) { ctx := context.Background() - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "10ns") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1ns") t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.config.lifespan", "1h") @@ -317,7 +317,7 @@ func TestLoginCodeStrategy(t *testing.T) { // with browser clients we redirect back to the UI with a new flow id as a query parameter require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) - require.NotEqual(t, s.flowID, resp.Request.URL.Query().Get("flow")) + require.Equal(t, s.flowID, resp.Request.URL.Query().Get("flow")) } }) }) diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index 896331578865..4a95341bddd6 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -130,6 +130,11 @@ func (s *Strategy) getCredentialsFromTraits(ctx context.Context, f *registration } func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) error { + s.deps.Audit(). + WithRequest(r). + WithField("registration_flow_id", f.ID). + Info("Registration with the code strategy started.") + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { return err } diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts index 5f1cebcfd83d..d03b441e9989 100644 --- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -20,170 +20,179 @@ context("Login error messages with code method", () => { ].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.useConfigProfile(profile) + cy.deleteMail() + cy.clearAllCookies() + + const email = gen.email() + cy.wrap(email).as("email") + cy.registerWithCode({ email }) + + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should show error message when account identifier does not exist", () => { + const email = gen.email() + + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).type(email) + cy.submitCodeForm() + + cy.url().should("contain", "login") + + cy.get('[data-testid="ui/message/4000029"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", ) }) - }) - beforeEach(() => { - cy.deleteMail() - cy.clearAllCookies() + 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.visit(route) - const email = gen.email() - cy.wrap(email).as("email") - cy.registerWithCode({ email }) + cy.submitCodeForm() - cy.deleteMail() - cy.clearAllCookies() - }) + 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", + ) - 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="code"]').type( + "invalid-code", + ) + cy.submitCodeForm() - cy.get( - 'form[data-testid="login-flow-code"] input[name="identifier"]', - ).type(email) - cy.submitCodeForm() + cy.get('[data-testid="ui/message/4010008"]').should( + "contain", + "The login code is invalid or has already been used. Please try again.", + ) + }) - cy.url().should("contain", "login") + 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.get('[data-testid="ui/message/4000028"]').should( - "contain", - "This account does not exist or has not setup sign in with code.", - ) - }) + cy.submitCodeForm() - it("should show error message when code is invalid", () => { - cy.get("@email").then((email) => { + cy.url().should("contain", "login") cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') .clear() - .type(email.toString()) - }) + .type(gen.email()) + cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( + "invalid-code", + ) + cy.submitCodeForm() - cy.submitCodeForm() + cy.get('[data-testid="ui/message/4000029"]').should( + "contain", + "This account does not exist or has not setup sign in with code.", + ) + }) - 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", - ) + it("should show error message when required fields are missing", () => { + cy.get("@email").then((email) => { + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).type(email.toString()) + }) - cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( - "invalid-code", - ) - cy.submitCodeForm() + cy.submitCodeForm() + cy.url().should("contain", "login") - cy.get('[data-testid="ui/message/4010008"]').should( - "contain", - "The login code is invalid or has already been used. Please try again.", - ) - }) + cy.removeAttribute( + ['form[data-testid="login-flow-code"] input[name="code"]'], + "required", + ) + cy.submitCodeForm() - 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.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property code is missing", + ) - 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.", - ) - }) + 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", + ) - it("should show error message when code is expired", () => { - cy.clearAllCookies() + cy.get('form[data-testid="login-flow-code"] input[name="identifier"]').clear() - cy.updateConfigFile((config) => { - config.selfservice.methods.code.config.lifespan = "1ns" - return config + cy.submitCodeForm() + cy.get('[data-testid="ui/message/4000002"]').should( + "contain", + "Property identifier is missing", + ) }) - cy.visit(route) + it("should show error message when code is expired", () => { + cy.updateConfigFile((config) => { + config.selfservice.methods.code = { + registration_enabled: true, + login_enabled: true, + config: { + lifespan: "1ns" + }, + } + return config + }).then(() => { + 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.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("@email").then((email) => { + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( + code, + ) + }) }) - }) - cy.get('[data-testid="ui/message/4040001"]').should( - "contain", - "The login flow expired", - ) + cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4010001"]').should( + "contain", + "The login flow expired", + ) - cy.updateConfigFile((config) => { - config.selfservice.methods.code.config.lifespan = "1h" - return config + cy.updateConfigFile((config) => { + config.selfservice.methods.code = { + registration_enabled: true, + login_enabled: true, + 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 index efd554eef982..176d581b59ee 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -8,7 +8,6 @@ context("Registration error messages with code method", () => { ;[ { route: express.registration, - login: express.login, app: "express" as "express", profile: "code", }, @@ -17,145 +16,141 @@ context("Registration error messages with code method", () => { // app: "react" as "react", // profile: "code", // }, - ].forEach(({ route, login, profile, app }) => { + ].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) - }) + beforeEach(() => { + cy.useConfigProfile(profile) + 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 code is invalid", () => { + const email = gen.email() - 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.", - ) - }) + 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", + ) - it("should show error message when code is expired", () => { - cy.updateConfigFile((config) => { - config.selfservice.methods.code.config.lifespan = "1ns" - return config + 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.", + ) }) - const email = gen.email() + 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.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.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(code) + ).type("invalid-code") cy.submitCodeForm() + + cy.get('[data-testid="ui/message/4000030"]').should( + "contain", + "The provided traits do not match the traits previously associated with this flow.", + ) }) - cy.get('[data-testid="ui/message/4040001"]').should( - "contain", - "The registration flow expired", - ) + it("should show error message when required fields are missing", () => { + const email = gen.email() - cy.updateConfigFile((config) => { - config.selfservice.methods.code.config.lifespan = "1h" - return config + 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", + ) }) - }) - 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( - [ + 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"]', - ], - "required", - ) - - cy.submitCodeForm() - cy.get('[data-testid="ui/message/4000002"]').should( - "contain", - "Property email is missing", - ) + ).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 + }) + }) }) }) }) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts index 6f7cd08c20b5..d802db6387a8 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -1,7 +1,6 @@ // 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" @@ -25,9 +24,6 @@ context("Registration success with code method", () => { 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", []) }) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 566a7393e271..ac5b01bf8e19 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -80,7 +80,7 @@ Cypress.Commands.add("proxy", (app: string) => { }) }) -Cypress.Commands.add("shortPrivilegedSessionTime", ({} = {}) => { +Cypress.Commands.add("shortPrivilegedSessionTime", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.privileged_session_max_age = "1ms" return config @@ -109,50 +109,50 @@ Cypress.Commands.add("setDefaultIdentitySchema", (id: string) => { }) }) -Cypress.Commands.add("longPrivilegedSessionTime", ({} = {}) => { +Cypress.Commands.add("longPrivilegedSessionTime", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.privileged_session_max_age = "5m" return config }) }) -Cypress.Commands.add("longVerificationLifespan", ({} = {}) => { +Cypress.Commands.add("longVerificationLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.lifespan = "1m" return config }) }) -Cypress.Commands.add("shortVerificationLifespan", ({} = {}) => { +Cypress.Commands.add("shortVerificationLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.lifespan = "1ms" return config }) }) -Cypress.Commands.add("sessionRequiresNo2fa", ({} = {}) => { +Cypress.Commands.add("sessionRequiresNo2fa", ({ } = {}) => { updateConfigFile((config) => { config.session.whoami.required_aal = "aal1" return config }) }) -Cypress.Commands.add("sessionRequires2fa", ({} = {}) => { +Cypress.Commands.add("sessionRequires2fa", ({ } = {}) => { updateConfigFile((config) => { config.session.whoami.required_aal = "highest_available" return config }) }) -Cypress.Commands.add("shortLinkLifespan", ({} = {}) => { +Cypress.Commands.add("shortLinkLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.methods.link.config.lifespan = "1ms" return config }) }) -Cypress.Commands.add("longLinkLifespan", ({} = {}) => { +Cypress.Commands.add("longLinkLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.methods.link.config.lifespan = "1m" return config }) }) -Cypress.Commands.add("shortCodeLifespan", ({} = {}) => { +Cypress.Commands.add("shortCodeLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1ms" return config @@ -173,28 +173,28 @@ Cypress.Commands.add("longLifespan", (strategy: Strategy) => { }) }) -Cypress.Commands.add("longCodeLifespan", ({} = {}) => { +Cypress.Commands.add("longCodeLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1m" return config }) }) -Cypress.Commands.add("shortCodeLifespan", ({} = {}) => { +Cypress.Commands.add("shortCodeLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1ms" return config }) }) -Cypress.Commands.add("longCodeLifespan", ({} = {}) => { +Cypress.Commands.add("longCodeLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1m" return config }) }) -Cypress.Commands.add("longRecoveryLifespan", ({} = {}) => { +Cypress.Commands.add("longRecoveryLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.recovery.lifespan = "1m" return config @@ -225,20 +225,20 @@ Cypress.Commands.add("setPostCodeRegistrationHooks", (hooks) => { cy.setupHooks("registration", "after", "code", hooks) }) -Cypress.Commands.add("shortLoginLifespan", ({} = {}) => { +Cypress.Commands.add("shortLoginLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.login.lifespan = "100ms" return config }) }) -Cypress.Commands.add("longLoginLifespan", ({} = {}) => { +Cypress.Commands.add("longLoginLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.login.lifespan = "1h" return config }) }) -Cypress.Commands.add("shortRecoveryLifespan", ({} = {}) => { +Cypress.Commands.add("shortRecoveryLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.recovery.lifespan = "1ms" return config @@ -253,7 +253,7 @@ Cypress.Commands.add("requireStrictAal", () => { }) }) -Cypress.Commands.add("useLaxAal", ({} = {}) => { +Cypress.Commands.add("useLaxAal", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.required_aal = "aal1" config.session.whoami.required_aal = "aal1" @@ -261,21 +261,21 @@ Cypress.Commands.add("useLaxAal", ({} = {}) => { }) }) -Cypress.Commands.add("disableVerification", ({} = {}) => { +Cypress.Commands.add("disableVerification", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.enabled = false return config }) }) -Cypress.Commands.add("enableVerification", ({} = {}) => { +Cypress.Commands.add("enableVerification", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.enabled = true return config }) }) -Cypress.Commands.add("enableRecovery", ({} = {}) => { +Cypress.Commands.add("enableRecovery", ({ } = {}) => { updateConfigFile((config) => { if (!config.selfservice.flows.recovery) { config.selfservice.flows.recovery = {} @@ -306,28 +306,28 @@ Cypress.Commands.add("disableRecoveryStrategy", (strategy: Strategy) => { }) }) -Cypress.Commands.add("disableRecovery", ({} = {}) => { +Cypress.Commands.add("disableRecovery", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.recovery.enabled = false return config }) }) -Cypress.Commands.add("disableRegistration", ({} = {}) => { +Cypress.Commands.add("disableRegistration", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.enabled = false return config }) }) -Cypress.Commands.add("enableRegistration", ({} = {}) => { +Cypress.Commands.add("enableRegistration", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.enabled = true return config }) }) -Cypress.Commands.add("useLaxAal", ({} = {}) => { +Cypress.Commands.add("useLaxAal", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.required_aal = "aal1" config.session.whoami.required_aal = "aal1" @@ -416,10 +416,11 @@ Cypress.Commands.add( }) .then(({ body }) => { if (!code) { + console.log("registration with code", body) expect( body.ui.nodes.find( (f) => - f.group === "default" && f.attributes.name === "traits.email", + f.group === "code" && f.attributes.name === "traits.email", ).attributes.value, ).to.eq(email) return cy.getRegistrationCodeFromEmail(email).then((code) => { @@ -723,21 +724,21 @@ Cypress.Commands.add( }, ) -Cypress.Commands.add("shortRegisterLifespan", ({} = {}) => { +Cypress.Commands.add("shortRegisterLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.lifespan = "100ms" return config }) }) -Cypress.Commands.add("longRegisterLifespan", ({} = {}) => { +Cypress.Commands.add("longRegisterLifespan", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.lifespan = "1h" return config }) }) -Cypress.Commands.add("browserReturnUrlOry", ({} = {}) => { +Cypress.Commands.add("browserReturnUrlOry", ({ } = {}) => { updateConfigFile((config) => { config.selfservice.allowed_return_urls = [ "https://www.ory.sh/", @@ -747,7 +748,7 @@ Cypress.Commands.add("browserReturnUrlOry", ({} = {}) => { }) }) -Cypress.Commands.add("remoteCourierRecoveryTemplates", ({} = {}) => { +Cypress.Commands.add("remoteCourierRecoveryTemplates", ({ } = {}) => { updateConfigFile((config) => { config.courier.templates = { recovery: { @@ -777,7 +778,7 @@ Cypress.Commands.add("remoteCourierRecoveryTemplates", ({} = {}) => { }) }) -Cypress.Commands.add("remoteCourierRecoveryCodeTemplates", ({} = {}) => { +Cypress.Commands.add("remoteCourierRecoveryCodeTemplates", ({ } = {}) => { updateConfigFile((config) => { config.courier.templates = { recovery_code: { diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index ed7365d83829..5ea61b182b4a 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -13,13 +13,16 @@ selfservice: after: password: hooks: - - hook: session + - + hook: session login: ui_url: http://localhost:4455/login after: code: - - hook: require_verified_address + hooks: + - + hook: require_verified_address error: ui_url: http://localhost:4455/error verification: @@ -33,6 +36,8 @@ selfservice: registration_enabled: true login_enabled: true enabled: true + config: + lifespan: 1h identity: schemas: From ca77a83a92578300554ba98b36a76215d659b9a0 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:19:18 +0200 Subject: [PATCH 11/24] style: format --- .../profiles/code/login/error.spec.ts | 17 +++--- test/e2e/cypress/support/commands.ts | 58 +++++++++---------- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts index d03b441e9989..8bd8e79db6f2 100644 --- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -132,7 +132,9 @@ context("Login error messages with code method", () => { "required", ) - cy.get('form[data-testid="login-flow-code"] input[name="identifier"]').clear() + cy.get( + 'form[data-testid="login-flow-code"] input[name="identifier"]', + ).clear() cy.submitCodeForm() cy.get('[data-testid="ui/message/4000002"]').should( @@ -147,7 +149,7 @@ context("Login error messages with code method", () => { registration_enabled: true, login_enabled: true, config: { - lifespan: "1ns" + lifespan: "1ns", }, } return config @@ -155,7 +157,6 @@ context("Login error messages with code method", () => { cy.visit(route) }) - cy.get("@email").then((email) => { cy.get( 'form[data-testid="login-flow-code"] input[name="identifier"]', @@ -167,9 +168,9 @@ context("Login error messages with code method", () => { 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.get( + 'form[data-testid="login-flow-code"] input[name="code"]', + ).type(code) }) }) @@ -185,14 +186,12 @@ context("Login error messages with code method", () => { registration_enabled: true, login_enabled: true, config: { - lifespan: "1h" + lifespan: "1h", }, } return config }) }) - - }) }) }) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index ac5b01bf8e19..e026f235360c 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -80,7 +80,7 @@ Cypress.Commands.add("proxy", (app: string) => { }) }) -Cypress.Commands.add("shortPrivilegedSessionTime", ({ } = {}) => { +Cypress.Commands.add("shortPrivilegedSessionTime", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.privileged_session_max_age = "1ms" return config @@ -109,50 +109,50 @@ Cypress.Commands.add("setDefaultIdentitySchema", (id: string) => { }) }) -Cypress.Commands.add("longPrivilegedSessionTime", ({ } = {}) => { +Cypress.Commands.add("longPrivilegedSessionTime", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.privileged_session_max_age = "5m" return config }) }) -Cypress.Commands.add("longVerificationLifespan", ({ } = {}) => { +Cypress.Commands.add("longVerificationLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.lifespan = "1m" return config }) }) -Cypress.Commands.add("shortVerificationLifespan", ({ } = {}) => { +Cypress.Commands.add("shortVerificationLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.lifespan = "1ms" return config }) }) -Cypress.Commands.add("sessionRequiresNo2fa", ({ } = {}) => { +Cypress.Commands.add("sessionRequiresNo2fa", ({} = {}) => { updateConfigFile((config) => { config.session.whoami.required_aal = "aal1" return config }) }) -Cypress.Commands.add("sessionRequires2fa", ({ } = {}) => { +Cypress.Commands.add("sessionRequires2fa", ({} = {}) => { updateConfigFile((config) => { config.session.whoami.required_aal = "highest_available" return config }) }) -Cypress.Commands.add("shortLinkLifespan", ({ } = {}) => { +Cypress.Commands.add("shortLinkLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.methods.link.config.lifespan = "1ms" return config }) }) -Cypress.Commands.add("longLinkLifespan", ({ } = {}) => { +Cypress.Commands.add("longLinkLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.methods.link.config.lifespan = "1m" return config }) }) -Cypress.Commands.add("shortCodeLifespan", ({ } = {}) => { +Cypress.Commands.add("shortCodeLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1ms" return config @@ -173,28 +173,28 @@ Cypress.Commands.add("longLifespan", (strategy: Strategy) => { }) }) -Cypress.Commands.add("longCodeLifespan", ({ } = {}) => { +Cypress.Commands.add("longCodeLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1m" return config }) }) -Cypress.Commands.add("shortCodeLifespan", ({ } = {}) => { +Cypress.Commands.add("shortCodeLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1ms" return config }) }) -Cypress.Commands.add("longCodeLifespan", ({ } = {}) => { +Cypress.Commands.add("longCodeLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1m" return config }) }) -Cypress.Commands.add("longRecoveryLifespan", ({ } = {}) => { +Cypress.Commands.add("longRecoveryLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.recovery.lifespan = "1m" return config @@ -225,20 +225,20 @@ Cypress.Commands.add("setPostCodeRegistrationHooks", (hooks) => { cy.setupHooks("registration", "after", "code", hooks) }) -Cypress.Commands.add("shortLoginLifespan", ({ } = {}) => { +Cypress.Commands.add("shortLoginLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.login.lifespan = "100ms" return config }) }) -Cypress.Commands.add("longLoginLifespan", ({ } = {}) => { +Cypress.Commands.add("longLoginLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.login.lifespan = "1h" return config }) }) -Cypress.Commands.add("shortRecoveryLifespan", ({ } = {}) => { +Cypress.Commands.add("shortRecoveryLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.recovery.lifespan = "1ms" return config @@ -253,7 +253,7 @@ Cypress.Commands.add("requireStrictAal", () => { }) }) -Cypress.Commands.add("useLaxAal", ({ } = {}) => { +Cypress.Commands.add("useLaxAal", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.required_aal = "aal1" config.session.whoami.required_aal = "aal1" @@ -261,21 +261,21 @@ Cypress.Commands.add("useLaxAal", ({ } = {}) => { }) }) -Cypress.Commands.add("disableVerification", ({ } = {}) => { +Cypress.Commands.add("disableVerification", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.enabled = false return config }) }) -Cypress.Commands.add("enableVerification", ({ } = {}) => { +Cypress.Commands.add("enableVerification", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.verification.enabled = true return config }) }) -Cypress.Commands.add("enableRecovery", ({ } = {}) => { +Cypress.Commands.add("enableRecovery", ({} = {}) => { updateConfigFile((config) => { if (!config.selfservice.flows.recovery) { config.selfservice.flows.recovery = {} @@ -306,28 +306,28 @@ Cypress.Commands.add("disableRecoveryStrategy", (strategy: Strategy) => { }) }) -Cypress.Commands.add("disableRecovery", ({ } = {}) => { +Cypress.Commands.add("disableRecovery", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.recovery.enabled = false return config }) }) -Cypress.Commands.add("disableRegistration", ({ } = {}) => { +Cypress.Commands.add("disableRegistration", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.enabled = false return config }) }) -Cypress.Commands.add("enableRegistration", ({ } = {}) => { +Cypress.Commands.add("enableRegistration", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.enabled = true return config }) }) -Cypress.Commands.add("useLaxAal", ({ } = {}) => { +Cypress.Commands.add("useLaxAal", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.settings.required_aal = "aal1" config.session.whoami.required_aal = "aal1" @@ -724,21 +724,21 @@ Cypress.Commands.add( }, ) -Cypress.Commands.add("shortRegisterLifespan", ({ } = {}) => { +Cypress.Commands.add("shortRegisterLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.lifespan = "100ms" return config }) }) -Cypress.Commands.add("longRegisterLifespan", ({ } = {}) => { +Cypress.Commands.add("longRegisterLifespan", ({} = {}) => { updateConfigFile((config) => { config.selfservice.flows.registration.lifespan = "1h" return config }) }) -Cypress.Commands.add("browserReturnUrlOry", ({ } = {}) => { +Cypress.Commands.add("browserReturnUrlOry", ({} = {}) => { updateConfigFile((config) => { config.selfservice.allowed_return_urls = [ "https://www.ory.sh/", @@ -748,7 +748,7 @@ Cypress.Commands.add("browserReturnUrlOry", ({ } = {}) => { }) }) -Cypress.Commands.add("remoteCourierRecoveryTemplates", ({ } = {}) => { +Cypress.Commands.add("remoteCourierRecoveryTemplates", ({} = {}) => { updateConfigFile((config) => { config.courier.templates = { recovery: { @@ -778,7 +778,7 @@ Cypress.Commands.add("remoteCourierRecoveryTemplates", ({ } = {}) => { }) }) -Cypress.Commands.add("remoteCourierRecoveryCodeTemplates", ({ } = {}) => { +Cypress.Commands.add("remoteCourierRecoveryCodeTemplates", ({} = {}) => { updateConfigFile((config) => { config.courier.templates = { recovery_code: { From 26690c3883eb6c696d98d228da84dc0595d5c944 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Mon, 21 Aug 2023 17:12:52 +0200 Subject: [PATCH 12/24] test(e2e): login success with code --- .../profiles/code/login/success.spec.ts | 175 ++++++++++++++++++ .../code/registration/success.spec.ts | 1 - test/e2e/cypress/support/commands.ts | 42 +++-- test/e2e/cypress/support/index.d.ts | 3 +- 4 files changed, 204 insertions(+), 17 deletions(-) create mode 100644 test/e2e/cypress/integration/profiles/code/login/success.spec.ts diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts new file mode 100644 index 000000000000..4e1905d1d347 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -0,0 +1,175 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" + +context("Login success with code method", () => { + ;[ + { + route: express.login, + app: "express" as "express", + profile: "code", + }, + // { + // route: react.registration, + // app: "react" as "react", + // profile: "code", + // }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + cy.proxy(app) + cy.setPostCodeRegistrationHooks([]) + cy.setupHooks("login", "after", "code", []) + }) + + beforeEach(() => { + const email = gen.email() + cy.wrap(email).as("email") + cy.registerWithCode({ email }) + + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should be able to sign in with code", () => { + cy.get("@email").then((email) => { + cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') + .clear() + .type(email.toString()) + cy.submitCodeForm() + + cy.getLoginCodeFromEmail(email.toString()).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) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + + it("should be able to resend login code", () => { + cy.get("@email").then((email) => { + cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') + .clear() + .type(email.toString()) + cy.submitCodeForm() + + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.wrap(code).as("code1") + }) + + cy.get("button[name=resend]").click() + + cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code1").then((code1) => { + cy.get("@code2").then((code2) => { + expect(code1).to.not.equal(code2) + }) + }) + + // attempt to submit code 1 + cy.get("@code1").then((code1) => { + cy.get('form[data-testid="login-flow-code"] input[name="code"]') + .clear() + .type(code1.toString()) + }) + + cy.get("button[name=method][value=code]").click() + + cy.get("[data-testid='ui/message/4010008']").contains( + "The login code is invalid or has already been used", + ) + + // attempt to submit code 2 + cy.get("@code2").then((code2) => { + cy.get('form[data-testid="login-flow-code"] input[name="code"]') + .clear() + .type(code2.toString()) + }) + + cy.get('button[name="method"][value="code"]').click() + + if (app === "express") { + cy.get('a[href*="sessions"').click() + } + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + + it("should be able to login to un-verfied email", () => { + const email = gen.email() + const email2 = gen.email() + + // Setup complex schema + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.complex.traits.schema.json", + ) + + cy.registerWithCode({ + email: email, + traits: { + "traits.username": Math.random().toString(36), + "traits.email2": email2, + }, + }) + cy.deleteMail({ atLeast: 2 }) + + cy.visit(route) + + cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') + .clear() + .type(email2) + cy.submitCodeForm() + + 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() + }) + + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + expect(session.identity.verifiable_addresses).to.have.length(2) + expect(session.identity.verifiable_addresses[0].status).to.equal( + "completed", + ) + expect(session.identity.verifiable_addresses[1].status).to.equal( + "completed", + ) + }, + ) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts index d802db6387a8..627458ce9d15 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -255,7 +255,6 @@ context("Registration success with code method", () => { } 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) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index e026f235360c..4465329c1600 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -383,7 +383,7 @@ Cypress.Commands.add( Cypress.Commands.add( "registerWithCode", - ({ email = gen.email(), code = undefined, query = {} } = {}) => { + ({ email = gen.email(), code = undefined, traits = {}, query = {} } = {}) => { console.log("Creating user account: ", { email }) cy.clearAllCookies() @@ -409,6 +409,7 @@ Cypress.Commands.add( body: mergeFields(form, { method: "code", "traits.email": email, + ...traits, ...(code && { code }), }), url: form.action, @@ -423,21 +424,32 @@ Cypress.Commands.add( f.group === "code" && 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, + + const expectedCount = + Object.keys(traits) + .map((k) => (k.includes("email") ? k : null)) + .filter(Boolean).length + 1 + + return cy + .getRegistrationCodeFromEmail(email, { + expectedCount: expectedCount, + }) + .then((code) => { + return cy.request({ + headers: { + Accept: "application/json", + }, + method: form.method, + body: mergeFields(form, { + method: "code", + "traits.email": email, + code, + ...traits, + }), + url: form.action, + followRedirect: false, + }) }) - }) } else { expect(body.session).to.contain(email) } diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index b6edfef4cd91..a30442ee68af 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -37,7 +37,7 @@ declare global { getSession(opts?: { expectAal?: "aal2" | "aal1" expectMethods?: Array< - "password" | "webauthn" | "lookup_secret" | "totp" + "password" | "webauthn" | "lookup_secret" | "totp" | "code" > }): Chainable @@ -78,6 +78,7 @@ declare global { registerWithCode(opts: { email: string code?: string + traits?: { [key: string]: any } query?: { [key: string]: string } }): Chainable> From 6e8a964f0cd54d88bf7b58e050161591543b80b5 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Tue, 22 Aug 2023 09:04:34 +0200 Subject: [PATCH 13/24] test(e2e): recovery on identity with code --- .../code/registration/success.spec.ts | 24 ++++++++++++++++++- test/e2e/profiles/code/.kratos.yml | 2 ++ .../code/identity.complex.traits.schema.json | 10 +++++++- .../profiles/code/identity.traits.schema.json | 7 +++++- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts index 627458ce9d15..7c43108150bc 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -10,6 +10,7 @@ context("Registration success with code method", () => { { route: express.registration, login: express.login, + recovery: express.recovery, app: "express" as "express", profile: "code", }, @@ -18,7 +19,7 @@ context("Registration success with code method", () => { // app: "react" as "react", // profile: "code", // }, - ].forEach(({ route, login, profile, app }) => { + ].forEach(({ route, login, recovery, profile, app }) => { describe(`for app ${app}`, () => { before(() => { cy.deleteMail() @@ -189,6 +190,27 @@ context("Registration success with code method", () => { }) }) + it("should be able to recover account when registered with code", () => { + const email = gen.email() + cy.registerWithCode({ email }) + + cy.clearAllCookies() + cy.visit(recovery) + + cy.get('input[name="email"]').type(email) + cy.get('button[name="method"][value="code"]').click() + + cy.recoveryEmailWithCode({ expect: { email } }) + cy.get('button[value="code"]').click() + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + // Try keep this test as the last one, as it updates the identity schema. it("should be able to use multiple identifiers to signup with and sign in to", () => { cy.setPostCodeRegistrationHooks([ { diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 5ea61b182b4a..204b16fcc6c7 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -30,6 +30,8 @@ selfservice: use: code ui_url: http://localhost:4455/verification recovery: + enabled: true + use: code ui_url: http://localhost:4455/recovery methods: code: diff --git a/test/e2e/profiles/code/identity.complex.traits.schema.json b/test/e2e/profiles/code/identity.complex.traits.schema.json index d6b5b817c510..9eda33789066 100644 --- a/test/e2e/profiles/code/identity.complex.traits.schema.json +++ b/test/e2e/profiles/code/identity.complex.traits.schema.json @@ -39,6 +39,9 @@ }, "verification": { "via": "email" + }, + "recovery": { + "via": "email" } } }, @@ -59,11 +62,16 @@ }, "verification": { "via": "email" + }, + "recovery": { + "via": "email" } } } }, - "required": ["email"] + "required": [ + "email" + ] } } } diff --git a/test/e2e/profiles/code/identity.traits.schema.json b/test/e2e/profiles/code/identity.traits.schema.json index 55fccdc481e6..268ae57da51a 100644 --- a/test/e2e/profiles/code/identity.traits.schema.json +++ b/test/e2e/profiles/code/identity.traits.schema.json @@ -24,11 +24,16 @@ }, "verification": { "via": "email" + }, + "recovery": { + "via": "email" } } } }, - "required": ["email"] + "required": [ + "email" + ] } } } From aeee834c92df917d0fe7001ab1560a7ec3c892d3 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Tue, 22 Aug 2023 14:14:31 +0200 Subject: [PATCH 14/24] fix: sdk login and registrion should contain code method --- .schema/openapi/patches/selfservice.yaml | 4 +++ .../client-go/model_update_login_flow_body.go | 30 +++++++++++++++++++ .../model_update_registration_flow_body.go | 30 +++++++++++++++++++ .../model_update_login_flow_body.go | 30 +++++++++++++++++++ .../model_update_registration_flow_body.go | 30 +++++++++++++++++++ spec/api.json | 8 +++++ 6 files changed, 132 insertions(+) diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index 102c1fe60deb..a966cf27401e 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -17,6 +17,7 @@ - "$ref": "#/components/schemas/updateRegistrationFlowWithPasswordMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithOidcMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" - op: add path: /components/schemas/updateRegistrationFlowBody/discriminator value: @@ -25,6 +26,7 @@ password: "#/components/schemas/updateRegistrationFlowWithPasswordMethod" oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + code: "#/components/schemas/updateRegistrationFlowWithCodeMethod" - op: add path: /components/schemas/registrationFlowState/enum value: @@ -44,6 +46,7 @@ - "$ref": "#/components/schemas/updateLoginFlowWithTotpMethod" - "$ref": "#/components/schemas/updateLoginFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" - op: add path: /components/schemas/updateLoginFlowBody/discriminator value: @@ -54,6 +57,7 @@ totp: "#/components/schemas/updateLoginFlowWithTotpMethod" webauthn: "#/components/schemas/updateLoginFlowWithWebAuthnMethod" lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod" + code: "#/components/schemas/updateLoginFlowWithCodeMethod" - op: add path: /components/schemas/loginFlowState/enum value: diff --git a/internal/client-go/model_update_login_flow_body.go b/internal/client-go/model_update_login_flow_body.go index 1aa032062a4b..36033328e78d 100644 --- a/internal/client-go/model_update_login_flow_body.go +++ b/internal/client-go/model_update_login_flow_body.go @@ -18,6 +18,7 @@ import ( // UpdateLoginFlowBody - struct for UpdateLoginFlowBody type UpdateLoginFlowBody struct { + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod @@ -25,6 +26,13 @@ type UpdateLoginFlowBody struct { UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod } +// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCodeMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithCodeMethod: v, + } +} + // UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -64,6 +72,19 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateLoginFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) + if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateLoginFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateLoginFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) if err == nil { @@ -131,6 +152,7 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateLoginFlowWithCodeMethod = nil dst.UpdateLoginFlowWithLookupSecretMethod = nil dst.UpdateLoginFlowWithOidcMethod = nil dst.UpdateLoginFlowWithPasswordMethod = nil @@ -147,6 +169,10 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateLoginFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) + } + if src.UpdateLoginFlowWithLookupSecretMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) } @@ -175,6 +201,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateLoginFlowWithCodeMethod != nil { + return obj.UpdateLoginFlowWithCodeMethod + } + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { return obj.UpdateLoginFlowWithLookupSecretMethod } diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go index 1a662fc62416..0e36a95f635f 100644 --- a/internal/client-go/model_update_registration_flow_body.go +++ b/internal/client-go/model_update_registration_flow_body.go @@ -18,11 +18,19 @@ import ( // UpdateRegistrationFlowBody - Update Registration Request Body type UpdateRegistrationFlowBody struct { + UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } +// UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithCodeMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithCodeMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithCodeMethod: v, + } +} + // UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithOidcMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithOidcMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -48,6 +56,19 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) + if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateRegistrationFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateRegistrationFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) if err == nil { @@ -89,6 +110,7 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateRegistrationFlowWithCodeMethod = nil dst.UpdateRegistrationFlowWithOidcMethod = nil dst.UpdateRegistrationFlowWithPasswordMethod = nil dst.UpdateRegistrationFlowWithWebAuthnMethod = nil @@ -103,6 +125,10 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRegistrationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithCodeMethod) + } + if src.UpdateRegistrationFlowWithOidcMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } @@ -123,6 +149,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return obj.UpdateRegistrationFlowWithCodeMethod + } + if obj.UpdateRegistrationFlowWithOidcMethod != nil { return obj.UpdateRegistrationFlowWithOidcMethod } diff --git a/internal/httpclient/model_update_login_flow_body.go b/internal/httpclient/model_update_login_flow_body.go index 1aa032062a4b..36033328e78d 100644 --- a/internal/httpclient/model_update_login_flow_body.go +++ b/internal/httpclient/model_update_login_flow_body.go @@ -18,6 +18,7 @@ import ( // UpdateLoginFlowBody - struct for UpdateLoginFlowBody type UpdateLoginFlowBody struct { + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod @@ -25,6 +26,13 @@ type UpdateLoginFlowBody struct { UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod } +// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCodeMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithCodeMethod: v, + } +} + // UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -64,6 +72,19 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateLoginFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) + if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateLoginFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateLoginFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) if err == nil { @@ -131,6 +152,7 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateLoginFlowWithCodeMethod = nil dst.UpdateLoginFlowWithLookupSecretMethod = nil dst.UpdateLoginFlowWithOidcMethod = nil dst.UpdateLoginFlowWithPasswordMethod = nil @@ -147,6 +169,10 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateLoginFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) + } + if src.UpdateLoginFlowWithLookupSecretMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) } @@ -175,6 +201,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateLoginFlowWithCodeMethod != nil { + return obj.UpdateLoginFlowWithCodeMethod + } + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { return obj.UpdateLoginFlowWithLookupSecretMethod } diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go index 1a662fc62416..0e36a95f635f 100644 --- a/internal/httpclient/model_update_registration_flow_body.go +++ b/internal/httpclient/model_update_registration_flow_body.go @@ -18,11 +18,19 @@ import ( // UpdateRegistrationFlowBody - Update Registration Request Body type UpdateRegistrationFlowBody struct { + UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } +// UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithCodeMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithCodeMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithCodeMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithCodeMethod: v, + } +} + // UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithOidcMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithOidcMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -48,6 +56,19 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error match := 0 + // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod + err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) + if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct + dst.UpdateRegistrationFlowWithCodeMethod = nil + } else { + match++ + } + } else { + dst.UpdateRegistrationFlowWithCodeMethod = nil + } + // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) if err == nil { @@ -89,6 +110,7 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { if match > 1 { // more than 1 match // reset to nil + dst.UpdateRegistrationFlowWithCodeMethod = nil dst.UpdateRegistrationFlowWithOidcMethod = nil dst.UpdateRegistrationFlowWithPasswordMethod = nil dst.UpdateRegistrationFlowWithWebAuthnMethod = nil @@ -103,6 +125,10 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { // Marshal data from the first non-nil pointers in the struct to JSON func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { + if src.UpdateRegistrationFlowWithCodeMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithCodeMethod) + } + if src.UpdateRegistrationFlowWithOidcMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } @@ -123,6 +149,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { if obj == nil { return nil } + if obj.UpdateRegistrationFlowWithCodeMethod != nil { + return obj.UpdateRegistrationFlowWithCodeMethod + } + if obj.UpdateRegistrationFlowWithOidcMethod != nil { return obj.UpdateRegistrationFlowWithOidcMethod } diff --git a/spec/api.json b/spec/api.json index e5f2261f347f..596ab6ab9984 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2400,6 +2400,7 @@ "updateLoginFlowBody": { "discriminator": { "mapping": { + "code": "#/components/schemas/updateLoginFlowWithCodeMethod", "lookup_secret": "#/components/schemas/updateLoginFlowWithLookupSecretMethod", "oidc": "#/components/schemas/updateLoginFlowWithOidcMethod", "password": "#/components/schemas/updateLoginFlowWithPasswordMethod", @@ -2423,6 +2424,9 @@ }, { "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" + }, + { + "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" } ] }, @@ -2666,6 +2670,7 @@ "description": "Update Registration Request Body", "discriminator": { "mapping": { + "code": "#/components/schemas/updateRegistrationFlowWithCodeMethod", "oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod", "password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod", "webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" @@ -2681,6 +2686,9 @@ }, { "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" + }, + { + "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" } ] }, From 4bdfc79d0554f76b90c3899eea3f79ba3abefb79 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Tue, 22 Aug 2023 14:15:04 +0200 Subject: [PATCH 15/24] test(e2e): spa flows on login and registration code --- .../profiles/code/login/error.spec.ts | 83 ++++----- .../profiles/code/login/success.spec.ts | 41 ++-- .../profiles/code/registration/error.spec.ts | 82 +++----- .../code/registration/success.spec.ts | 176 ++++++------------ test/e2e/profiles/code/.kratos.yml | 4 +- 5 files changed, 136 insertions(+), 250 deletions(-) diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts index 8bd8e79db6f2..635e4415f4cb 100644 --- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -12,19 +12,20 @@ context("Login error messages with code method", () => { app: "express" as "express", profile: "code", }, - // { - // route: react.login, - // app: "react" as "react", - // profile: "code", - // }, + { + route: react.login, + app: "react" as "react", + profile: "code", + }, ].forEach(({ route, profile, app }) => { describe(`for app ${app}`, () => { before(() => { cy.proxy(app) + cy.useConfigProfile(profile) + cy.deleteMail() }) beforeEach(() => { - cy.useConfigProfile(profile) cy.deleteMail() cy.clearAllCookies() @@ -40,9 +41,7 @@ context("Login error messages with code method", () => { 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.get('input[name="identifier"]').type(email) cy.submitCodeForm() cy.url().should("contain", "login") @@ -55,9 +54,7 @@ context("Login error messages with code method", () => { 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.get('input[name="identifier"]').clear().type(email.toString()) }) cy.submitCodeForm() @@ -68,9 +65,7 @@ context("Login error messages with code method", () => { "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.get('input[name="code"]').type("invalid-code") cy.submitCodeForm() cy.get('[data-testid="ui/message/4010008"]').should( @@ -81,20 +76,14 @@ context("Login error messages with code method", () => { 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.get('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.get('input[name="identifier"]').clear().type(gen.email()) + cy.get('input[name="code"]').type("invalid-code") cy.submitCodeForm() cy.get('[data-testid="ui/message/4000029"]').should( @@ -105,18 +94,13 @@ context("Login error messages with code method", () => { it("should show error message when required fields are missing", () => { cy.get("@email").then((email) => { - cy.get( - 'form[data-testid="login-flow-code"] input[name="identifier"]', - ).type(email.toString()) + cy.get('input[name="identifier"]').type(email.toString()) }) cy.submitCodeForm() cy.url().should("contain", "login") - cy.removeAttribute( - ['form[data-testid="login-flow-code"] input[name="code"]'], - "required", - ) + cy.removeAttribute(['input[name="code"]'], "required") cy.submitCodeForm() cy.get('[data-testid="ui/message/4000002"]').should( @@ -124,17 +108,10 @@ context("Login error messages with code method", () => { "Property code is missing", ) - 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.get('input[name="code"]').type("invalid-code") + cy.removeAttribute(['input[name="identifier"]'], "required") - cy.get( - 'form[data-testid="login-flow-code"] input[name="identifier"]', - ).clear() + cy.get('input[name="identifier"]').clear() cy.submitCodeForm() cy.get('[data-testid="ui/message/4000002"]').should( @@ -158,9 +135,7 @@ context("Login error messages with code method", () => { }) cy.get("@email").then((email) => { - cy.get( - 'form[data-testid="login-flow-code"] input[name="identifier"]', - ).type(email.toString()) + cy.get('input[name="identifier"]').type(email.toString()) }) cy.submitCodeForm() @@ -168,18 +143,24 @@ context("Login error messages with code method", () => { 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.get('input[name="code"]').type(code) }) }) cy.submitCodeForm() - cy.get('[data-testid="ui/message/4010001"]').should( - "contain", - "The login flow expired", - ) + // the react app does not show the error message for 410 errors + // it just creates a new flow + if (app === "express") { + cy.get('[data-testid="ui/message/4010001"]').should( + "contain", + "The login flow expired", + ) + } else { + cy.get("input[name=identifier]").should("be.visible") + } + + cy.noSession() cy.updateConfigFile((config) => { config.selfservice.methods.code = { diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts index 4e1905d1d347..5f523cf105ed 100644 --- a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -1,8 +1,9 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { gen } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" context("Login success with code method", () => { ;[ @@ -11,11 +12,11 @@ context("Login success with code method", () => { app: "express" as "express", profile: "code", }, - // { - // route: react.registration, - // app: "react" as "react", - // profile: "code", - // }, + { + route: react.login, + app: "react" as "react", + profile: "code", + }, ].forEach(({ route, profile, app }) => { describe(`for app ${app}`, () => { before(() => { @@ -38,15 +39,11 @@ context("Login success with code method", () => { it("should be able to sign in with code", () => { cy.get("@email").then((email) => { - cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') - .clear() - .type(email.toString()) + cy.get('input[name="identifier"]').clear().type(email.toString()) cy.submitCodeForm() cy.getLoginCodeFromEmail(email.toString()).then((code) => { - cy.get( - 'form[data-testid="login-flow-code"] input[name="code"]', - ).type(code) + cy.get('input[name="code"]').type(code) cy.get("button[name=method][value=code]").click() }) @@ -68,9 +65,7 @@ context("Login success with code method", () => { it("should be able to resend login code", () => { cy.get("@email").then((email) => { - cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') - .clear() - .type(email.toString()) + cy.get('input[name="identifier"]').clear().type(email.toString()) cy.submitCodeForm() cy.getLoginCodeFromEmail(email.toString()).then((code) => { @@ -91,9 +86,7 @@ context("Login success with code method", () => { // attempt to submit code 1 cy.get("@code1").then((code1) => { - cy.get('form[data-testid="login-flow-code"] input[name="code"]') - .clear() - .type(code1.toString()) + cy.get('input[name="code"]').clear().type(code1.toString()) }) cy.get("button[name=method][value=code]").click() @@ -104,9 +97,7 @@ context("Login success with code method", () => { // attempt to submit code 2 cy.get("@code2").then((code2) => { - cy.get('form[data-testid="login-flow-code"] input[name="code"]') - .clear() - .type(code2.toString()) + cy.get('input[name="code"]').clear().type(code2.toString()) }) cy.get('button[name="method"][value="code"]').click() @@ -146,15 +137,11 @@ context("Login success with code method", () => { cy.visit(route) - cy.get('form[data-testid="login-flow-code"] input[name="identifier"]') - .clear() - .type(email2) + cy.get('input[name="identifier"]').clear().type(email2) cy.submitCodeForm() cy.getLoginCodeFromEmail(email2).then((code) => { - cy.get('form[data-testid="login-flow-code"] input[name="code"]').type( - code, - ) + cy.get('input[name="code"]').type(code) cy.get("button[name=method][value=code]").click() }) diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts index 176d581b59ee..684019fa3cb7 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -1,6 +1,6 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { gen } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" @@ -11,19 +11,20 @@ context("Registration error messages with code method", () => { app: "express" as "express", profile: "code", }, - // { - // route: react.registration, - // app: "react" as "react", - // profile: "code", - // }, + { + route: react.registration, + app: "react" as "react", + profile: "code", + }, ].forEach(({ route, profile, app }) => { describe(`for app ${app}`, () => { before(() => { cy.proxy(app) + cy.useConfigProfile(profile) + cy.deleteMail() }) beforeEach(() => { - cy.useConfigProfile(profile) cy.deleteMail() cy.clearAllCookies() cy.visit(route) @@ -32,9 +33,7 @@ context("Registration error messages with code method", () => { 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.get('input[name="traits.email"]').type(email) cy.submitCodeForm() cy.url().should("contain", "registration") @@ -43,9 +42,7 @@ context("Registration error messages with code method", () => { "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.get(' input[name="code"]').type("invalid-code") cy.submitCodeForm() cy.get('[data-testid="ui/message/4040003"]').should( @@ -57,20 +54,14 @@ context("Registration error messages with code method", () => { 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.get('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"]', - ) + cy.get('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.get(' input[name="code"]').type("invalid-code") cy.submitCodeForm() cy.get('[data-testid="ui/message/4000030"]').should( @@ -82,17 +73,12 @@ context("Registration error messages with code method", () => { 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.get('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.removeAttribute(['input[name="code"]'], "required") cy.submitCodeForm() cy.get('[data-testid="ui/message/4000002"]').should( @@ -100,18 +86,9 @@ context("Registration error messages with code method", () => { "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.get('input[name="traits.email"]').clear() + cy.get('input[name="code"]').type("invalid-code") + cy.removeAttribute(['input[name="traits.email"]'], "required") cy.submitCodeForm() cy.get('[data-testid="ui/message/4000002"]').should( @@ -128,23 +105,26 @@ context("Registration error messages with code method", () => { const email = gen.email() - cy.get( - 'form[data-testid="registration-flow-code"] input[name="traits.email"]', - ).type(email) + cy.get(' 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('input[name="code"]').type(code) cy.submitCodeForm() }) - cy.get('[data-testid="ui/message/4040001"]').should( - "contain", - "The registration flow expired", - ) + // in the react spa app we don't show the 410 gone error. we create a new flow. + if (app === "express") { + cy.get('[data-testid="ui/message/4040001"]').should( + "contain", + "The registration flow expired", + ) + } else { + cy.get('input[name="traits.email"]').should("be.visible") + } + + cy.noSession() cy.updateConfigFile((config) => { config.selfservice.methods.code.config.lifespan = "1h" diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts index 7c43108150bc..eaf734b144ed 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { gen } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" @@ -14,19 +14,19 @@ context("Registration success with code method", () => { app: "express" as "express", profile: "code", }, - // { - // route: react.registration, - // app: "react" as "react", - // profile: "code", - // }, + { + route: react.registration, + login: react.login, + recovery: react.recovery, + app: "react" as "react", + profile: "code", + }, ].forEach(({ route, login, recovery, profile, app }) => { describe(`for app ${app}`, () => { before(() => { cy.deleteMail() cy.useConfigProfile(profile) cy.proxy(app) - cy.setPostCodeRegistrationHooks([]) - cy.setupHooks("login", "after", "code", []) }) beforeEach(() => { @@ -35,63 +35,10 @@ context("Registration success with code method", () => { 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.get(` input[name='traits.email']`).type(email) cy.submitCodeForm() @@ -101,15 +48,11 @@ context("Registration success with code method", () => { 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.get(` input[name='traits.email']`).should("have.value", email) + cy.get(` input[name='method'][value='code'][type='hidden']`).should( + "exist", + ) + cy.get(` button[name='resend'][value='code']`).click() cy.getRegistrationCodeFromEmail(email).then((code) => { cy.wrap(code).as("code2") @@ -117,11 +60,7 @@ context("Registration success with code method", () => { 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.get('input[name="code"]').clear().type(code1.toString()) cy.submitCodeForm() cy.get('[data-testid="ui/message/4040003"]').should( @@ -131,17 +70,10 @@ context("Registration success with code method", () => { }) cy.get("@code2").then((code2) => { - cy.get( - 'form[data-testid="registration-flow-code"] input[name="code"]', - ) - .clear() - .type(code2.toString()) + cy.get('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 @@ -152,34 +84,53 @@ context("Registration success with code method", () => { }) it("should sign up and be logged in with session hook", () => { - cy.setPostCodeRegistrationHooks([ - { - hook: "session", - }, - ]) + const email = gen.email() + + cy.get(` input[name='traits.email']`).type(email) + + cy.submitCodeForm() + + cy.url().should("contain", "registration") + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.get(` input[name=code]`).type(code) + cy.get("button[name=method][value=code]").click() + }) + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(1) + expect(identity.verifiable_addresses[0].status).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to sign up without session hook", () => { + cy.setPostCodeRegistrationHooks([]) const email = gen.email() - cy.get( - "form[data-testid='registration-flow-code'] input[name='traits.email']", - ).type(email) + cy.get(` 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(` 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.visit(login) + cy.get(` input[name=identifier]`).type(email) + cy.get("button[name=method][value=code]").click() + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get(`input[name = code]`).type(code) + cy.get("button[name=method][value=code]").click() + }) + + cy.deleteMail({ atLeast: 1 }) cy.getSession().should((session) => { const { identity } = session @@ -225,21 +176,15 @@ context("Registration success with code method", () => { cy.visit(route) - cy.get( - "form[data-testid='registration-flow-code'] input[name='traits.username']", - ).type(Math.random().toString(36)) + cy.get(`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) + cy.get(` 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.get(` input[name='traits.email2']`).type(email2) cy.submitCodeForm() @@ -247,9 +192,7 @@ context("Registration success with code method", () => { 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(`input[name=code]`).type(code) cy.get("button[name=method][value=code]").click() }, ) @@ -260,21 +203,14 @@ context("Registration success with code method", () => { // 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(` 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(`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) => { const { identity } = session diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 204b16fcc6c7..05820e0002e9 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -11,7 +11,7 @@ selfservice: registration: ui_url: http://localhost:4455/registration after: - password: + code: hooks: - hook: session @@ -34,6 +34,8 @@ selfservice: use: code ui_url: http://localhost:4455/recovery methods: + password: + enabled: false code: registration_enabled: true login_enabled: true From 2d488981a694d83c5ea27409c4ea0df1046235bf Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 23 Aug 2023 17:59:23 +0200 Subject: [PATCH 16/24] chore: rename config keys and return status 400 on first flow submit --- driver/config/config.go | 12 ++---- driver/config/config_test.go | 13 ++---- driver/registry_default.go | 4 +- driver/registry_default_registration.go | 2 +- driver/registry_default_test.go | 4 +- embedx/config.schema.json | 9 +---- ...7133700000000_identity_login_code.down.sql | 1 - ...700000000_identity_login_code.mysql.up.sql | 1 - ...707133700000000_identity_login_code.up.sql | 1 - ...000001_identity_registration_code.down.sql | 1 - ...01_identity_registration_code.mysql.up.sql | 1 - ...00000001_identity_registration_code.up.sql | 1 - selfservice/flow/request.go | 6 +-- selfservice/flow/request_test.go | 11 +++-- selfservice/strategy/code/strategy_login.go | 4 +- .../strategy/code/strategy_login_test.go | 26 +++++++----- .../strategy/code/strategy_recovery_test.go | 4 +- .../strategy/code/strategy_registration.go | 4 +- .../code/strategy_registration_test.go | 22 +++++++--- .../strategy/link/strategy_recovery_test.go | 12 +++--- .../profiles/code/login/success.spec.ts | 2 +- test/e2e/cypress/support/commands.ts | 11 ++++- test/e2e/run.sh | 40 +++++++++---------- 23 files changed, 97 insertions(+), 95 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index f0e585f9d744..c0614d636894 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -229,9 +229,8 @@ type ( Config json.RawMessage `json:"config"` } SelfServiceStrategyCode struct { - RegistrationEnabled bool `json:"registration_enabled"` - LoginEnabled bool `json:"login_enabled"` *SelfServiceStrategy + PasswordlessEnabled bool `json:"passwordless_enabled"` } Schema struct { ID string `json:"id" koanf:"id"` @@ -780,21 +779,18 @@ func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrate 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) + passwordlessKey := fmt.Sprintf("%s.passwordless_enabled", basePath) s := &SelfServiceStrategyCode{ SelfServiceStrategy: &SelfServiceStrategy{ Enabled: pp.Bool(enabledKey), Config: json.RawMessage(config), }, - RegistrationEnabled: pp.Bool(registrationKey), - LoginEnabled: pp.Bool(loginKey), + PasswordlessEnabled: pp.Bool(passwordlessKey), } if !pp.Exists(enabledKey) { - s.RegistrationEnabled = false - s.LoginEnabled = false + s.PasswordlessEnabled = false s.Enabled = true } return s diff --git a/driver/config/config_test.go b/driver/config/config_test.go index feb115c00f7c..dee0baa5e657 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -507,8 +507,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) }, }, @@ -524,8 +523,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) }, }, @@ -541,8 +539,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) }, }, { @@ -569,9 +566,7 @@ func TestViperProvider_Defaults(t *testing.T) { assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled) assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled) assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).LoginEnabled) - assert.False(t, p.SelfServiceCodeStrategy(ctx).RegistrationEnabled) - + assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled) assert.False(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)) assert.False(t, p.SelfServiceFlowVerificationNotifyUnknownRecipients(ctx)) }) diff --git a/driver/registry_default.go b/driver/registry_default.go index b69c39b3bd22..514409f5bd1e 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -331,7 +331,7 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} { func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool { switch id { case identity.CredentialsTypeCodeAuth.String(): - return m.Config().SelfServiceCodeStrategy(ctx).RegistrationEnabled + return m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled default: return m.Config().SelfServiceStrategy(ctx, id).Enabled } @@ -340,7 +340,7 @@ func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id st func (m *RegistryDefault) strategyLoginEnabled(ctx context.Context, id string) bool { switch id { case identity.CredentialsTypeCodeAuth.String(): - return m.Config().SelfServiceCodeStrategy(ctx).LoginEnabled + return m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled default: return m.Config().SelfServiceStrategy(ctx, id).Enabled } diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 5ea0f7d93345..0f6f7c6f05ea 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -12,7 +12,7 @@ import ( ) func (m *RegistryDefault) PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) (b []registration.PostHookPrePersistExecutor) { - if credentialsType == identity.CredentialsTypeCodeAuth && m.Config().SelfServiceCodeStrategy(ctx).RegistrationEnabled { + if credentialsType == identity.CredentialsTypeCodeAuth && m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled { b = append(b, m.HookCodeAddressVerifier()) } diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 3a4be63a768c..533cdd621a9a 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -653,7 +653,7 @@ func TestDriverDefault_Strategies(t *testing.T) { { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.registration_enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) }, expect: []string{"password", "code"}, }, @@ -706,7 +706,7 @@ func TestDriverDefault_Strategies(t *testing.T) { { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.login_enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) }, expect: []string{"password", "code"}, }, diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 0bcb38e2a902..7c3687eb22c5 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1299,14 +1299,9 @@ "type": "object", "additionalProperties": false, "properties": { - "login_enabled": { + "passwordless_enabled": { "type": "boolean", - "title": "Enables Login with Code Method", - "default": false - }, - "registration_enabled": { - "type": "boolean", - "title": "Enables Registration with Code Method", + "title": "Enables Login and Registration with the Code Method", "default": false }, "enabled": { diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql index 79a48193bfe8..3738073d0aa9 100644 --- a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql @@ -1,4 +1,3 @@ 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 index cdb888626d54..9ddc6c72d8ac 100644 --- a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql @@ -27,4 +27,3 @@ 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 index 448dd5e0c257..7df0e9b00e21 100644 --- a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql @@ -26,4 +26,3 @@ CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, 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 index cca834d74de3..d4211e92a776 100644 --- a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql @@ -1,4 +1,3 @@ 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 index 6a5e20cdfb81..36f049d58c1a 100644 --- a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql @@ -25,4 +25,3 @@ CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registratio 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 index 9ac21a49e60f..0dc3a9879341 100644 --- a/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql +++ b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql @@ -25,4 +25,3 @@ CREATE INDEX identity_registration_codes_nid_flow_id_idx ON identity_registratio 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/selfservice/flow/request.go b/selfservice/flow/request.go index bb595351d8b2..ef1f9ea3c617 100644 --- a/selfservice/flow/request.go +++ b/selfservice/flow/request.go @@ -113,10 +113,8 @@ func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, a 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 + case RegistrationFlow, LoginFlow: + ok = d.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled case VerificationFlow, RecoveryFlow: ok = d.Config().SelfServiceCodeStrategy(ctx).Enabled default: diff --git a/selfservice/flow/request_test.go b/selfservice/flow/request_test.go index 4fa39a61bc46..17a7d9e2d0d3 100644 --- a/selfservice/flow/request_test.go +++ b/selfservice/flow/request_test.go @@ -108,7 +108,7 @@ func TestMethodCodeEnabledAndAllowed(t *testing.T) { })) t.Run("login code allowed", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.login_enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) currentFlow <- flow.LoginFlow res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) require.NoError(t, err) @@ -117,7 +117,7 @@ func TestMethodCodeEnabledAndAllowed(t *testing.T) { }) t.Run("login code not allowed", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.login_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false) currentFlow <- flow.LoginFlow res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) require.NoError(t, err) @@ -129,7 +129,7 @@ func TestMethodCodeEnabledAndAllowed(t *testing.T) { }) t.Run("registration code allowed", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.registration_enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) currentFlow <- flow.RegistrationFlow res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) require.NoError(t, err) @@ -138,7 +138,7 @@ func TestMethodCodeEnabledAndAllowed(t *testing.T) { }) t.Run("registration code not allowed", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.registration_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false) currentFlow <- flow.RegistrationFlow res, err := ts.Client().PostForm(ts.URL, url.Values{"method": {"code"}}) require.NoError(t, err) @@ -150,8 +150,7 @@ func TestMethodCodeEnabledAndAllowed(t *testing.T) { }) 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.passwordless_enabled", false) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) for _, f := range []flow.FlowName{flow.RecoveryFlow, flow.VerificationFlow} { diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index e73e9610def4..9345c834590a 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -136,7 +136,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.HandleLoginError(w, r, f, &p, err) } - codeManager := NewCodeStateManager[*login.Flow, *updateLoginFlowWithCodeMethod](f, s, &p) + codeManager := NewCodeStateManager(f, s, &p) codeManager.SetCreateCodeHandler(func(ctx context.Context, f *login.Flow, strategy *Strategy, p *updateLoginFlowWithCodeMethod) error { strategy.deps.Audit(). @@ -196,7 +196,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } if x.IsJSONRequest(r) { - strategy.deps.Writer().Write(w, r, f) + strategy.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) } else { http.Redirect(w, r, f.AppendTo(strategy.deps.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) } diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index b29cbac41647..3f4a631a9e62 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -29,7 +29,7 @@ func TestLoginCodeStrategy(t *testing.T) { 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, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), true) conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) @@ -131,12 +131,18 @@ func TestLoginCodeStrategy(t *testing.T) { return s } - require.EqualValues(t, http.StatusOK, resp.StatusCode) - if mustHaveSession { resp, err = s.client.Get(s.testServer.URL + session.RouteWhoami) require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) + } else { + // SPAs need to be informed that the login has not yet completed using status 400. + // Browser clients will redirect back to the login URL. + if isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } } return s @@ -155,9 +161,7 @@ func TestLoginCodeStrategy(t *testing.T) { isSPA: false, }, } { - t.Run("test="+tc.d, func(t *testing.T) { - t.Run("case=should be able to log in with code", func(t *testing.T) { // create login flow s := createLoginFlow(ctx, t, public, tc.isSPA) @@ -237,7 +241,6 @@ func TestLoginCodeStrategy(t *testing.T) { require.NoError(t, err) assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "account does not exist or has not setup sign in with code") } - }) }) @@ -317,7 +320,13 @@ func TestLoginCodeStrategy(t *testing.T) { // with browser clients we redirect back to the UI with a new flow id as a query parameter require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) - require.Equal(t, s.flowID, resp.Request.URL.Query().Get("flow")) + lf, _, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + body, err := json.Marshal(lf) + require.NoError(t, err) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), "flow expired 0.00 minutes ago") } }) }) @@ -419,9 +428,6 @@ func TestLoginCodeStrategy(t *testing.T) { require.NotNil(t, va) require.True(t, va.Verified) }) - }) - } - } diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go index 92e484f324b3..10d5ae6f5135 100644 --- a/selfservice/strategy/code/strategy_recovery_test.go +++ b/selfservice/strategy/code/strategy_recovery_test.go @@ -1070,7 +1070,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) action := gjson.Get(body, "ui.action").String() @@ -1102,7 +1102,7 @@ func TestRecovery(t *testing.T) { v.Set("email", recoveryEmail) }) - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) action := gjson.Get(body, "ui.action").String() diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index 4a95341bddd6..dc4dce7a6b8d 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -148,7 +148,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return s.HandleRegistrationError(w, r, f, &p, err) } - codeManager := NewCodeStateManager[*registration.Flow, *updateRegistrationFlowWithCodeMethod](f, s, &p) + codeManager := NewCodeStateManager(f, s, &p) codeManager.SetCreateCodeHandler(func(ctx context.Context, f *registration.Flow, strategy *Strategy, p *updateRegistrationFlowWithCodeMethod) error { strategy.deps.Logger(). @@ -196,7 +196,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } if x.IsJSONRequest(r) { - strategy.deps.Writer().Write(w, r, f) + strategy.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) } else { http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther) } diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index f88af0d0ef80..edac27c8506c 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -44,7 +44,7 @@ func TestRegistrationCodeStrategyDisabled(t *testing.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) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), false) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) @@ -87,7 +87,7 @@ func TestRegistrationCodeStrategy(t *testing.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, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), true) conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ @@ -148,7 +148,11 @@ func TestRegistrationCodeStrategy(t *testing.T) { return s } - require.Equal(t, http.StatusOK, resp.StatusCode) + if isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() assert.NotEmptyf(t, csrfToken, "%s", body) require.Equal(t, email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) @@ -241,7 +245,11 @@ func TestRegistrationCodeStrategy(t *testing.T) { s := createRegistrationFlow(ctx, t, public, tc.isSPA) s = registerNewUser(ctx, t, s, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusOK, resp.StatusCode) + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.EqualValues(t, http.StatusOK, resp.StatusCode) + } csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() require.NotEmptyf(t, csrfToken, "%s", body) require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) @@ -263,7 +271,11 @@ func TestRegistrationCodeStrategy(t *testing.T) { s = submitOTP(ctx, t, reg, s, func(v *url.Values) { v.Set("resend", "code") }, tc.isSPA, func(ctx context.Context, t *testing.T, s *state, body string, resp *http.Response) { - require.Equal(t, http.StatusOK, resp.StatusCode) + if tc.isSPA { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + } else { + require.Equal(t, http.StatusOK, resp.StatusCode) + } csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() require.NotEmptyf(t, csrfToken, "%s", body) require.Containsf(t, gjson.Get(body, "ui.messages").String(), "An email containing a code has been sent to the email address you provided.", "%s", body) diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 07625da76d7e..71518cda33be 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -834,8 +834,8 @@ func TestRecovery(t *testing.T) { recoveryEmail := testhelpers.RandomEmail() createIdentityToRecover(t, reg, recoveryEmail) - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -851,7 +851,7 @@ func TestRecovery(t *testing.T) { assert.Contains(t, cookies, "ory_kratos_session") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", recoveryEmail) } @@ -867,8 +867,8 @@ func TestRecovery(t *testing.T) { recoveryEmail := testhelpers.RandomEmail() createIdentityToRecover(t, reg, recoveryEmail) - var check = func(t *testing.T, actual string) { - message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account") + check := func(t *testing.T, actual string) { + message := testhelpers.CourierExpectMessage(ctx, t, reg, recoveryEmail, "Recover access to your account") recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1) cl := testhelpers.NewClientWithCookies(t) @@ -884,7 +884,7 @@ func TestRecovery(t *testing.T) { assert.NotContains(t, cookies, "ory_kratos_session") } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("email", recoveryEmail) } diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts index 5f523cf105ed..acbce5547899 100644 --- a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -133,7 +133,7 @@ context("Login success with code method", () => { "traits.email2": email2, }, }) - cy.deleteMail({ atLeast: 2 }) + cy.deleteMail({ atLeast: 1 }) cy.visit(route) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 4465329c1600..b6b4226c6e46 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -18,6 +18,7 @@ import dayjs from "dayjs" import YAML from "yamljs" import { Strategy } from "." import { OryKratosConfiguration } from "./config" +import { UiNode, UiNodeAttributes } from "@ory/kratos-client" const configFile = "kratos.generated.yml" @@ -414,14 +415,17 @@ Cypress.Commands.add( }), url: form.action, followRedirect: false, + failOnStatusCode: false, }) .then(({ body }) => { if (!code) { console.log("registration with code", body) expect( body.ui.nodes.find( - (f) => - f.group === "code" && f.attributes.name === "traits.email", + (f: UiNode) => + f.group === "code" && + "name" in f.attributes && + f.attributes.name === "traits.email", ).attributes.value, ).to.eq(email) @@ -1270,6 +1274,9 @@ Cypress.Commands.add( mailItem = response.body.mailItems.find((m: any) => m.toAddresses.includes(email), ) + if (!mailItem) { + return req + } } else { mailItem = response.body.mailItems[0] } diff --git a/test/e2e/run.sh b/test/e2e/run.sh index deb35875b725..accffdc54f9c 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" @@ -219,19 +219,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 ( From a0c8e75304277278a2292ab03b70869714c84410 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:33:39 +0200 Subject: [PATCH 17/24] fix: verification handler test sql error --- selfservice/flow/verification/handler_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/selfservice/flow/verification/handler_test.go b/selfservice/flow/verification/handler_test.go index 092f659e4a36..4eac94367b59 100644 --- a/selfservice/flow/verification/handler_test.go +++ b/selfservice/flow/verification/handler_test.go @@ -23,6 +23,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/session" "github.com/ory/kratos/x" @@ -203,6 +204,7 @@ func TestPostFlow(t *testing.T) { Type: "browser", ExpiresAt: time.Now().Add(1 * time.Hour), IssuedAt: time.Now(), + State: flow.StateChooseMethod, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) @@ -227,6 +229,8 @@ func TestPostFlow(t *testing.T) { IdentityID: uuid.NullUUID{UUID: uuid.Must(uuid.NewV4()), Valid: true}, AMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, }, + SessionID: uuid.NullUUID{UUID: s.ID, Valid: true}, + State: flow.StatePassedChallenge, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) @@ -254,6 +258,7 @@ func TestPostFlow(t *testing.T) { ExpiresAt: time.Now().Add(1 * time.Hour), IssuedAt: time.Now(), OAuth2LoginChallenge: hydra.FakeValidLoginChallenge, + State: flow.StateChooseMethod, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) @@ -264,5 +269,4 @@ func TestPostFlow(t *testing.T) { assert.Equal(t, f.ID.String(), resp.Request.URL.Query().Get("flow")) }) }) - } From 558bfe5431adbf70f5d477ae60882dbe02464ae5 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:33:54 +0200 Subject: [PATCH 18/24] chore: restore e2e test script --- test/e2e/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index accffdc54f9c..3ea3e5bcab8e 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -137,7 +137,7 @@ prepare() { nc -zv localhost 4446 && exit 1 nc -zv localhost 4455 && exit 1 nc -zv localhost 19006 && exit 1 - # nc -zv localhost 4456 && exit 1 + nc -zv localhost 4456 && exit 1 nc -zv localhost 4458 && exit 1 nc -zv localhost 4744 && exit 1 nc -zv localhost 4745 && exit 1 From 35769fc1e5cd5e89e267491902d884390192c254 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:45:05 +0200 Subject: [PATCH 19/24] fix(test): verification fake strategy --- selfservice/flow/verification/fake_strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/selfservice/flow/verification/fake_strategy.go b/selfservice/flow/verification/fake_strategy.go index 58f6b1e5af5c..d497fb5111f3 100644 --- a/selfservice/flow/verification/fake_strategy.go +++ b/selfservice/flow/verification/fake_strategy.go @@ -13,6 +13,8 @@ import ( type FakeStrategy struct{} +var _ Strategy = new(FakeStrategy) + func (f FakeStrategy) VerificationStrategyID() string { return "fake" } @@ -32,3 +34,7 @@ func (f FakeStrategy) Verify(_ http.ResponseWriter, _ *http.Request, _ *Flow) (e func (f FakeStrategy) SendVerificationEmail(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress) error { return nil } + +func (f FakeStrategy) NodeGroup() node.UiNodeGroup { + return "fake" +} From 4da4887a8c533ca73a967a164fe708476a47fd4b Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Fri, 25 Aug 2023 10:01:11 +0200 Subject: [PATCH 20/24] feat: login and registration email template config keys --- embedx/config.schema.json | 565 ++++++++++++++++++++++++++++++-------- 1 file changed, 448 insertions(+), 117 deletions(-) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 7c3687eb22c5..be8dddc87f9b 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -43,7 +43,10 @@ "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", @@ -53,7 +56,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceSessionIssuerHook": { "type": "object", @@ -63,7 +68,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceRequireVerifiedAddressHook": { "type": "object", @@ -73,7 +80,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceShowVerificationUIHook": { "type": "object", @@ -83,7 +92,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "webHookAuthBasicAuthProperties": { "properties": { @@ -103,11 +114,17 @@ } }, "additionalProperties": false, - "required": ["user", "password"] + "required": [ + "user", + "password" + ] } }, "additionalProperties": false, - "required": ["type", "config"] + "required": [ + "type", + "config" + ] }, "httpRequestConfig": { "type": "object", @@ -115,7 +132,9 @@ "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?://" }, @@ -180,15 +199,25 @@ "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", @@ -227,7 +256,10 @@ "const": true } }, - "required": ["ignore", "parse"] + "required": [ + "ignore", + "parse" + ] } }, "url": { @@ -288,30 +320,46 @@ "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", @@ -344,7 +392,9 @@ "essential": true }, "acr": { - "values": ["urn:mace:incommon:iap:silver"] + "values": [ + "urn:mace:incommon:iap:silver" + ] } } } @@ -392,7 +442,9 @@ "properties": { "id": { "type": "string", - "examples": ["google"] + "examples": [ + "google" + ] }, "provider": { "title": "Provider", @@ -419,7 +471,9 @@ "linkedin", "lark" ], - "examples": ["google"] + "examples": [ + "google" + ] }, "label": { "title": "Optional string which will be used when generating labels for UI buttons.", @@ -434,17 +488,23 @@ "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", @@ -461,7 +521,10 @@ "type": "array", "items": { "type": "string", - "examples": ["offline_access", "profile"] + "examples": [ + "offline_access", + "profile" + ] } }, "microsoft_tenant": { @@ -480,21 +543,30 @@ "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", @@ -509,7 +581,12 @@ } }, "additionalProperties": false, - "required": ["id", "provider", "client_id", "mapper_url"], + "required": [ + "id", + "provider", + "client_id", + "mapper_url" + ], "allOf": [ { "if": { @@ -518,17 +595,23 @@ "const": "microsoft" } }, - "required": ["provider"] + "required": [ + "provider" + ] }, "then": { - "required": ["microsoft_tenant"] + "required": [ + "microsoft_tenant" + ] }, "else": { "not": { "properties": { "microsoft_tenant": {} }, - "required": ["microsoft_tenant"] + "required": [ + "microsoft_tenant" + ] } } }, @@ -539,7 +622,9 @@ "const": "apple" } }, - "required": ["provider"] + "required": [ + "provider" + ] }, "then": { "not": { @@ -549,7 +634,9 @@ "minLength": 1 } }, - "required": ["client_secret"] + "required": [ + "client_secret" + ] }, "required": [ "apple_private_key_id", @@ -558,7 +645,9 @@ ] }, "else": { - "required": ["client_secret"], + "required": [ + "client_secret" + ], "allOf": [ { "not": { @@ -568,7 +657,9 @@ "minLength": 1 } }, - "required": ["apple_team_id"] + "required": [ + "apple_team_id" + ] } }, { @@ -579,7 +670,9 @@ "minLength": 1 } }, - "required": ["apple_private_key_id"] + "required": [ + "apple_private_key_id" + ] } }, { @@ -590,7 +683,9 @@ "minLength": 1 } }, - "required": ["apple_private_key"] + "required": [ + "apple_private_key" + ] } } ] @@ -731,7 +826,10 @@ "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": { @@ -891,7 +989,9 @@ "path": { "title": "Path to PEM-encoded Fle", "type": "string", - "examples": ["path/to/file.pem"] + "examples": [ + "path/to/file.pem" + ] }, "base64": { "title": "Base64 Encoded Inline", @@ -939,7 +1039,9 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] }, "valid": { "additionalProperties": false, @@ -949,7 +1051,9 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] } } }, @@ -999,7 +1103,9 @@ "selfservice": { "type": "object", "additionalProperties": false, - "required": ["default_browser_return_url"], + "required": [ + "default_browser_return_url" + ], "properties": { "default_browser_return_url": { "$ref": "#/definitions/defaultReturnTo" @@ -1033,20 +1139,30 @@ "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" @@ -1095,14 +1211,20 @@ "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" @@ -1121,14 +1243,20 @@ "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" @@ -1154,7 +1282,9 @@ "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": { @@ -1166,7 +1296,11 @@ "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" @@ -1175,7 +1309,10 @@ "title": "Verification Strategy", "description": "The strategy to use for verification requests", "type": "string", - "enum": ["link", "code"], + "enum": [ + "link", + "code" + ], "default": "code" }, "notify_unknown_recipients": { @@ -1202,7 +1339,9 @@ "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": { @@ -1214,7 +1353,11 @@ "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" @@ -1223,7 +1366,10 @@ "title": "Recovery Strategy", "description": "The strategy to use for recovery requests", "type": "string", - "enum": ["link", "code"], + "enum": [ + "link", + "code" + ], "default": "code" }, "notify_unknown_recipients": { @@ -1243,7 +1389,9 @@ "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" } } @@ -1282,14 +1430,20 @@ "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" + ] } } } @@ -1319,7 +1473,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } } @@ -1442,13 +1600,17 @@ "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", @@ -1456,7 +1618,9 @@ "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", @@ -1477,32 +1641,57 @@ "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": {} }, + "origin": { + "not": {} + }, "origins": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } } } } @@ -1518,10 +1707,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "then": { - "required": ["config"] + "required": [ + "config" + ] } }, "oidc": { @@ -1544,7 +1737,9 @@ "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", @@ -1636,6 +1831,42 @@ }, "verification_code": { "$ref": "#/definitions/courierTemplates" + }, + "registration_code": { + "additionalProperties": false, + "type": "object", + "properties": { + "valid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + } + }, + "required": [ + "email" + ] + } + } + }, + "login_code": { + "additionalProperties": false, + "type": "object", + "properties": { + "valid": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + } + }, + "required": [ + "email" + ] + } + } } } }, @@ -1643,19 +1874,27 @@ "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", "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", "type": "string", - "enum": ["smtp", "http"], + "enum": [ + "smtp", + "http" + ], "default": "smtp" }, "http": { @@ -1712,7 +1951,9 @@ "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", @@ -1736,7 +1977,9 @@ "default": "localhost" } }, - "required": ["connection_uri"], + "required": [ + "connection_uri" + ], "additionalProperties": false }, "sms": { @@ -1761,7 +2004,9 @@ "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?:\\/\\/.*" }, @@ -1803,14 +2048,19 @@ }, "additionalProperties": false }, - "required": ["url", "method"], + "required": [ + "url", + "method" + ], "additionalProperties": false } }, "additionalProperties": false } }, - "required": ["smtp"], + "required": [ + "smtp" + ], "additionalProperties": false }, "oauth2_provider": { @@ -1872,7 +2122,9 @@ "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", @@ -1886,7 +2138,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4434], + "examples": [ + 4434 + ], "default": 4434 }, "socket": { @@ -1945,7 +2199,9 @@ ] }, "uniqueItems": true, - "default": ["*"], + "default": [ + "*" + ], "examples": [ [ "https://example.com", @@ -1957,7 +2213,13 @@ "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": [ @@ -1988,7 +2250,9 @@ "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" } @@ -2031,7 +2295,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4433], + "examples": [ + 4433 + ], "default": 4433 }, "socket": { @@ -2081,7 +2347,10 @@ "format": { "description": "The log format can either be text or JSON.", "type": "string", - "enum": ["json", "text"] + "enum": [ + "json", + "text" + ] } }, "additionalProperties": false @@ -2122,7 +2391,9 @@ "id": { "title": "The schema's ID.", "type": "string", - "examples": ["employee"] + "examples": [ + "employee" + ] }, "url": { "type": "string", @@ -2136,11 +2407,16 @@ ] } }, - "required": ["id", "url"] + "required": [ + "id", + "url" + ] } } }, - "required": ["schemas"], + "required": [ + "schemas" + ], "additionalProperties": false }, "secrets": { @@ -2189,7 +2465,10 @@ "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.", @@ -2245,7 +2524,9 @@ "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", @@ -2267,7 +2548,11 @@ "description": "One of the values: noop, aes, xchacha20-poly1305", "type": "string", "default": "noop", - "enum": ["noop", "aes", "xchacha20-poly1305"] + "enum": [ + "noop", + "aes", + "xchacha20-poly1305" + ] } } }, @@ -2291,7 +2576,11 @@ "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" } }, @@ -2318,7 +2607,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "cookie": { "type": "object", @@ -2349,7 +2642,11 @@ "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 @@ -2360,7 +2657,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } }, @@ -2369,7 +2670,9 @@ "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" @@ -2393,7 +2696,9 @@ "type": "integer", "minimum": 0, "maximum": 65535, - "examples": [4434], + "examples": [ + 4434 + ], "default": 0 }, "config": { @@ -2462,10 +2767,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] } }, - "required": ["verification"] + "required": [ + "verification" + ] }, { "properties": { @@ -2475,21 +2784,31 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] } }, - "required": ["recovery"] + "required": [ + "recovery" + ] } ] } }, - "required": ["flows"] + "required": [ + "flows" + ] } }, - "required": ["selfservice"] + "required": [ + "selfservice" + ] }, "then": { - "required": ["courier"] + "required": [ + "courier" + ] } }, { @@ -2508,21 +2827,33 @@ ] } }, - "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 } From 42ac455c9798d3498d5f874c477c8f0b72578e83 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 29 Aug 2023 09:58:43 +0200 Subject: [PATCH 21/24] chore: code review for `code` strategy (magic code login) (#3456) --- Makefile | 8 +- coverage/.gitignore | 1 + driver/registry_default_registration.go | 2 +- identity/credentials.go | 6 - identity/credentials_code.go | 2 +- identity/extension_credentials.go | 3 +- identity/extension_credentials_test.go | 15 + .../extension/credentials/code.schema.json | 20 ++ persistence/sql/persister_code.go | 123 ++++++++ persistence/sql/persister_login.go | 125 -------- persistence/sql/persister_login_code.go | 69 +++++ persistence/sql/persister_recovery.go | 144 --------- persistence/sql/persister_recovery_code.go | 84 ++++++ persistence/sql/persister_registration.go | 136 --------- .../sql/persister_registration_code.go | 76 +++++ persistence/sql/persister_verification.go | 154 ---------- .../sql/persister_verification_code.go | 77 +++++ schema/errors.go | 6 +- selfservice/flow/request.go | 7 +- selfservice/strategy/code/code_login.go | 25 +- selfservice/strategy/code/code_login_test.go | 81 +++++ selfservice/strategy/code/code_recovery.go | 25 +- .../strategy/code/code_recovery_test.go | 28 +- .../strategy/code/code_registration.go | 25 +- .../strategy/code/code_registration_test.go | 80 +++++ selfservice/strategy/code/code_sender.go | 4 + ...ification_code.go => code_verification.go} | 11 + .../strategy/code/code_verification_test.go | 81 +++++ selfservice/strategy/code/strategy.go | 167 ++--------- selfservice/strategy/code/strategy_login.go | 279 +++++++++--------- .../strategy/code/strategy_login_test.go | 25 +- .../strategy/code/strategy_registration.go | 237 ++++++++------- .../code/strategy_registration_test.go | 50 +++- .../strategy/code/strategy_verification.go | 4 - .../strategy/password/registration_test.go | 2 +- .../profiles/code/login/error.spec.ts | 10 +- .../profiles/code/login/success.spec.ts | 11 +- .../profiles/code/registration/error.spec.ts | 28 +- .../code/registration/success.spec.ts | 75 ++--- test/e2e/cypress/support/commands.ts | 43 +-- test/e2e/cypress/support/config.d.ts | 3 +- test/e2e/cypress/support/index.d.ts | 3 +- test/e2e/profiles/code/.kratos.yml | 3 +- 43 files changed, 1252 insertions(+), 1106 deletions(-) create mode 100644 coverage/.gitignore create mode 100644 identity/stub/extension/credentials/code.schema.json create mode 100644 persistence/sql/persister_code.go create mode 100644 persistence/sql/persister_login_code.go create mode 100644 persistence/sql/persister_recovery_code.go create mode 100644 persistence/sql/persister_registration_code.go create mode 100644 persistence/sql/persister_verification_code.go create mode 100644 selfservice/strategy/code/code_login_test.go create mode 100644 selfservice/strategy/code/code_registration_test.go rename selfservice/strategy/code/{verification_code.go => code_verification.go} (93%) create mode 100644 selfservice/strategy/code/code_verification_test.go diff --git a/Makefile b/Makefile index d1eb8fae5ef1..2759e18aae2e 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,13 @@ test-short: .PHONY: test-coverage test-coverage: .bin/go-acc .bin/goveralls - go-acc -o coverage.out ./... -- -v -failfast -timeout=20m -tags sqlite + go-acc -o coverage.out ./... -- -v -failfast -timeout=20m -tags sqlite,json1 + +.PHONY: test-coverage-next +test-coverage-next: .bin/go-acc .bin/goveralls + go test -short -failfast -timeout=20m -tags sqlite,json1 -cover ./... --args test.gocoverdir="$$PWD/coverage" + go tool covdata percent -i=coverage + go tool covdata textfmt -i=./coverage -o coverage.new.out # Generates the SDK .PHONY: sdk diff --git a/coverage/.gitignore b/coverage/.gitignore new file mode 100644 index 000000000000..72e8ffc0db8a --- /dev/null +++ b/coverage/.gitignore @@ -0,0 +1 @@ +* diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 0f6f7c6f05ea..89ed5e656c74 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -39,7 +39,7 @@ func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context, } if len(b) == initialHookCount { - // since we don't want merging hooks defined in a specific strategy and global hooks + // since we don't want merging hooks defined in a specific strategy and // global hooks are added only if no strategy specific hooks are defined for _, v := range m.getHooks(config.HookGlobal, m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, config.HookGlobal)) { if hook, ok := v.(registration.PostHookPostPersistExecutor); ok { diff --git a/identity/credentials.go b/identity/credentials.go index 29283b29aa66..6ccbe867c89a 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -168,12 +168,6 @@ 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 b6fc4a14b4fc..184479ae1700 100644 --- a/identity/credentials_code.go +++ b/identity/credentials_code.go @@ -14,7 +14,7 @@ const ( CodeAddressTypePhone CodeAddressType = AddressTypePhone ) -// CredentialsCode represents a one time login/registraiton code +// CredentialsCode represents a one time login/registration code // // swagger:model identityCredentialsCode type CredentialsCode struct { diff --git a/identity/extension_credentials.go b/identity/extension_credentials.go index 7885abf10bce..69fb53810fbc 100644 --- a/identity/extension_credentials.go +++ b/identity/extension_credentials.go @@ -41,7 +41,6 @@ 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) } @@ -64,7 +63,7 @@ func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s sch return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) } - r.setIdentifier(CredentialsTypeCodeAuth, value, CredentialsIdentifierAddressType(AddressTypeEmail)) + r.setIdentifier(CredentialsTypeCodeAuth, value, AddressTypeEmail) // case f.AddCase(AddressTypePhone): // if !jsonschema.Formats["tel"](value) { // return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) diff --git a/identity/extension_credentials_test.go b/identity/extension_credentials_test.go index cf580c6a0c10..95cd9d000c6a 100644 --- a/identity/extension_credentials_test.go +++ b/identity/extension_credentials_test.go @@ -72,6 +72,21 @@ func TestSchemaExtensionCredentials(t *testing.T) { }, ct: identity.CredentialsTypeWebAuthn, }, + { + doc: `{"email":"foo@ory.sh"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expect: []string{"foo@ory.sh"}, + ct: identity.CredentialsTypeCodeAuth, + }, + { + doc: `{"email":"FOO@ory.sh"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expect: []string{"foo@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"not-foo@ory.sh"}, + }, + ct: identity.CredentialsTypeCodeAuth, + }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { c := jsonschema.NewCompiler() diff --git a/identity/stub/extension/credentials/code.schema.json b/identity/stub/extension/credentials/code.schema.json new file mode 100644 index 000000000000..bef244bc9ae5 --- /dev/null +++ b/identity/stub/extension/credentials/code.schema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + } + } +} diff --git a/persistence/sql/persister_code.go b/persistence/sql/persister_code.go new file mode 100644 index 000000000000..3b8103a36361 --- /dev/null +++ b/persistence/sql/persister_code.go @@ -0,0 +1,123 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "crypto/subtle" + "fmt" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +type oneTimeCodeProvider interface { + GetID() uuid.UUID + Validate() error + TableName(ctx context.Context) string + GetHMACCode() string +} + +type codeOptions struct { + IdentityID *uuid.UUID +} + +type codeOption func(o *codeOptions) + +func withCheckIdentityID(id uuid.UUID) codeOption { + return func(o *codeOptions) { + o.IdentityID = &id + } +} + +func useOneTimeCode[P any, U interface { + *P + oneTimeCodeProvider +}](ctx context.Context, p *Persister, flowID uuid.UUID, userProvidedCode string, flowTableName string, foreignKeyName string, opts ...codeOption) (U, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.useOneTimeCode") + defer span.End() + + o := new(codeOptions) + for _, opt := range opts { + opt(o) + } + + var target U + nid := p.NetworkID(ctx) + if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + //#nosec G201 -- TableName is static + if err := tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), flowID, nid).Exec(); err != nil { + return err + } + + var submitCount int + // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. + //#nosec G201 -- TableName is static + if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), flowID, nid).First(&submitCount)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction + return nil + } + return err + } + + // This check prevents parallel brute force attacks by checking the submit count inside this database + // transaction. If the flow has been submitted more than 5 times, the transaction is aborted (regardless of + // whether the code was correct or not) and we thus give no indication whether the supplied code was correct or + // not. For more explanation see [this comment](https://github.com/ory/kratos/pull/2645#discussion_r984732899). + if submitCount > 5 { + return errors.WithStack(code.ErrCodeSubmittedTooOften) + } + + var codes []U + codesQuery := tx.Where(fmt.Sprintf("nid = ? AND %s = ?", foreignKeyName), nid, flowID) + if o.IdentityID != nil { + codesQuery = codesQuery.Where("identity_id = ?", *o.IdentityID) + } + + if err := sqlcon.HandleError(codesQuery.All(&codes)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction and reset the submit count. + return nil + } + + return err + } + + secrets: + for _, secret := range p.r.Config().SecretsSession(ctx) { + suppliedCode := []byte(p.hmacValueWithSecret(ctx, userProvidedCode, secret)) + for i := range codes { + c := codes[i] + if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 { + // Not the supplied code + continue + } + target = c + break secrets + } + } + + if target.Validate() != nil { + // Return no error, as that would roll back the transaction + return nil + } + + //#nosec G201 -- TableName is static + return tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", target.TableName(ctx)), time.Now().UTC(), target.GetID(), nid).Exec() + }); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := target.Validate(); err != nil { + return nil, err + } + + return target, nil +} diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go index d34a832167f5..ec1da55babbb 100644 --- a/persistence/sql/persister_login.go +++ b/persistence/sql/persister_login.go @@ -5,21 +5,16 @@ 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) @@ -88,123 +83,3 @@ 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.UTC().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_login_code.go b/persistence/sql/persister_login_code.go new file mode 100644 index 000000000000..3d5dd027826d --- /dev/null +++ b/persistence/sql/persister_login_code.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateLoginCode(ctx context.Context, params *code.CreateLoginCodeParams) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginCode") + defer span.End() + + now := time.Now().UTC() + loginCode := &code.LoginCode{ + IdentityID: params.IdentityID, + Address: params.Address, + AddressType: params.AddressType, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return loginCode, nil +} + +func (p *Persister) UseLoginCode(ctx context.Context, flowID uuid.UUID, identityID uuid.UUID, userProvidedCode string) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseLoginCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.LoginCode, *code.LoginCode](ctx, p, flowID, userProvidedCode, new(login.Flow).TableName(ctx), "selfservice_login_flow_id", withCheckIdentityID(identityID)) + if err != nil { + return nil, err + } + + return codeRow, nil +} + +func (p *Persister) GetUsedLoginCode(ctx context.Context, flowID uuid.UUID) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedLoginCode") + defer span.End() + + var loginCode code.LoginCode + if err := p.Connection(ctx).Where("selfservice_login_flow_id = ? AND nid = ? AND used_at IS NOT NULL", flowID, p.NetworkID(ctx)).First(&loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return &loginCode, nil +} + +func (p *Persister) DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteLoginCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_login_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.LoginCode{}) +} diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go index 34539832d254..8ac81cd009a5 100644 --- a/persistence/sql/persister_recovery.go +++ b/persistence/sql/persister_recovery.go @@ -5,7 +5,6 @@ package sql import ( "context" - "crypto/subtle" "fmt" "time" @@ -16,9 +15,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/persistence/sql/update" - "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" - "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/x/sqlcon" ) @@ -137,144 +134,3 @@ func (p *Persister) DeleteExpiredRecoveryFlows(ctx context.Context, expiresAt ti } return nil } - -func (p *Persister) CreateRecoveryCode(ctx context.Context, dto *code.CreateRecoveryCodeParams) (*code.RecoveryCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryCode") - defer span.End() - - now := time.Now() - - recoveryCode := &code.RecoveryCode{ - ID: uuid.Nil, - CodeHMAC: p.hmacValue(ctx, dto.RawCode), - ExpiresAt: now.UTC().Add(dto.ExpiresIn), - IssuedAt: now, - CodeType: dto.CodeType, - FlowID: dto.FlowID, - NID: p.NetworkID(ctx), - IdentityID: dto.IdentityID, - } - - if dto.RecoveryAddress != nil { - recoveryCode.RecoveryAddress = dto.RecoveryAddress - recoveryCode.RecoveryAddressID = uuid.NullUUID{ - UUID: dto.RecoveryAddress.ID, - Valid: true, - } - } - - // This should not create the request eagerly because otherwise we might accidentally create an address that isn't - // supposed to be in the database. - if err := p.GetConnection(ctx).Create(recoveryCode); err != nil { - return nil, err - } - - return recoveryCode, nil -} - -// UseRecoveryCode attempts to "use" the supplied code in the flow -// -// If the supplied code matched a code from the flow, no error is returned -// If an invalid code was submitted with this flow more than 5 times, an error is returned -// TODO: Extract the business logic to a new service/manager (https://github.com/ory/kratos/issues/2785) -func (p *Persister) UseRecoveryCode(ctx context.Context, fID uuid.UUID, codeVal string) (*code.RecoveryCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRecoveryCode") - defer span.End() - - var recoveryCode *code.RecoveryCode - - nid := p.NetworkID(ctx) - - flowTableName := new(recovery.Flow).TableName(ctx) - - if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { - //#nosec G201 -- TableName is static - if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), fID, nid).Exec()); err != nil { - return err - } - - var submitCount int - // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. - //#nosec G201 -- TableName is static - if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), fID, nid).First(&submitCount)); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - - // This check prevents parallel brute force attacks to generate the recovery code - // by checking the submit count inside this database transaction. - // If the flow has been submitted more than 5 times, the transaction is aborted (regardless of whether the code was correct or not) - // and we thus give no indication whether the supplied code was correct or not. See also https://github.com/ory/kratos/pull/2645#discussion_r984732899 - if submitCount > 5 { - return errors.WithStack(code.ErrCodeSubmittedTooOften) - } - - var recoveryCodes []code.RecoveryCode - if err = sqlcon.HandleError(tx.Where("nid = ? AND selfservice_recovery_flow_id = ?", nid, fID).All(&recoveryCodes)); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - - secrets: - for _, secret := range p.r.Config().SecretsSession(ctx) { - suppliedCode := []byte(p.hmacValueWithSecret(ctx, codeVal, secret)) - for i := range recoveryCodes { - code := recoveryCodes[i] - if subtle.ConstantTimeCompare([]byte(code.CodeHMAC), suppliedCode) == 0 { - // Not the supplied code - continue - } - recoveryCode = &code - break secrets - } - } - - if recoveryCode == nil || !recoveryCode.IsValid() { - // Return no error, as that would roll back the transaction - return nil - } - - var ra identity.RecoveryAddress - if err := tx.Where("id = ? AND nid = ?", recoveryCode.RecoveryAddressID, nid).First(&ra); err != nil { - if err = sqlcon.HandleError(err); !errors.Is(err, sqlcon.ErrNoRows) { - return err - } - } - recoveryCode.RecoveryAddress = &ra - - //#nosec G201 -- TableName is static - return sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", recoveryCode.TableName(ctx)), time.Now().UTC(), recoveryCode.ID, nid).Exec()) - })); err != nil { - return nil, err - } - - if recoveryCode == nil { - return nil, errors.WithStack(code.ErrCodeNotFound) - } - - if recoveryCode.IsExpired() { - return nil, errors.WithStack(flow.NewFlowExpiredError(recoveryCode.ExpiresAt)) - } - - if recoveryCode.WasUsed() { - return nil, errors.WithStack(code.ErrCodeAlreadyUsed) - } - - return recoveryCode, nil -} - -func (p *Persister) DeleteRecoveryCodesOfFlow(ctx context.Context, fID uuid.UUID) error { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRecoveryCodesOfFlow") - defer span.End() - - //#nosec G201 -- TableName is static - return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE selfservice_recovery_flow_id = ? AND nid = ?", new(code.RecoveryCode).TableName(ctx)), fID, p.NetworkID(ctx)).Exec() -} diff --git a/persistence/sql/persister_recovery_code.go b/persistence/sql/persister_recovery_code.go new file mode 100644 index 000000000000..725b9578a205 --- /dev/null +++ b/persistence/sql/persister_recovery_code.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateRecoveryCode(ctx context.Context, params *code.CreateRecoveryCodeParams) (*code.RecoveryCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRecoveryCode") + defer span.End() + + now := time.Now() + recoveryCode := &code.RecoveryCode{ + ID: uuid.Nil, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + ExpiresAt: now.UTC().Add(params.ExpiresIn), + IssuedAt: now, + CodeType: params.CodeType, + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + IdentityID: params.IdentityID, + } + + if params.RecoveryAddress != nil { + recoveryCode.RecoveryAddress = params.RecoveryAddress + recoveryCode.RecoveryAddressID = uuid.NullUUID{ + UUID: params.RecoveryAddress.ID, + Valid: true, + } + } + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(recoveryCode); err != nil { + return nil, err + } + + return recoveryCode, nil +} + +// UseRecoveryCode attempts to "use" the supplied code in the flow +// +// If the supplied code matched a code from the flow, no error is returned +// If an invalid code was submitted with this flow more than 5 times, an error is returned +func (p *Persister) UseRecoveryCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string) (*code.RecoveryCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRecoveryCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.RecoveryCode, *code.RecoveryCode](ctx, p, flowID, userProvidedCode, new(recovery.Flow).TableName(ctx), "selfservice_recovery_flow_id") + if err != nil { + return nil, err + } + + var ra identity.RecoveryAddress + if err := sqlcon.HandleError(p.GetConnection(ctx).Where("id = ? AND nid = ?", codeRow.RecoveryAddressID, p.NetworkID(ctx)).First(&ra)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // This is ok, it can happen when an administrator initiates account recovery. This works even if the + // user has no recovery address! + } else { + return nil, err + } + } + codeRow.RecoveryAddress = &ra + + return codeRow, nil +} + +func (p *Persister) DeleteRecoveryCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRecoveryCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_recovery_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.RecoveryCode{}) +} diff --git a/persistence/sql/persister_registration.go b/persistence/sql/persister_registration.go index 7bd665fcedf0..fe7e25ceeac3 100644 --- a/persistence/sql/persister_registration.go +++ b/persistence/sql/persister_registration.go @@ -5,21 +5,15 @@ 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 { @@ -70,133 +64,3 @@ 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_registration_code.go b/persistence/sql/persister_registration_code.go new file mode 100644 index 000000000000..5c9ac909838c --- /dev/null +++ b/persistence/sql/persister_registration_code.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/bxcodec/faker/v3/support/slice" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateRegistrationCode(ctx context.Context, params *code.CreateRegistrationCodeParams) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateRegistrationCode") + defer span.End() + + now := time.Now().UTC() + registrationCode := &code.RegistrationCode{ + Address: params.Address, + AddressType: params.AddressType, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(registrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return registrationCode, nil +} + +func (p *Persister) UseRegistrationCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string, addresses ...string) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseRegistrationCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.RegistrationCode, *code.RegistrationCode](ctx, p, flowID, userProvidedCode, new(registration.Flow).TableName(ctx), "selfservice_registration_flow_id") + if err != nil { + return nil, err + } + + // ensure that the identifiers extracted from the traits are contained in the registration code + if !slice.Contains(addresses, codeRow.Address) { + return nil, errors.WithStack(code.ErrCodeNotFound) + } + + return codeRow, nil +} + +func (p *Persister) GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedRegistrationCode") + defer span.End() + + var registrationCode code.RegistrationCode + if err := p.Connection(ctx).Where("selfservice_registration_flow_id = ? AND used_at IS NOT NULL AND nid = ?", flowID, p.NetworkID(ctx)).First(®istrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + + return ®istrationCode, nil +} + +func (p *Persister) DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRegistrationCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_registration_flow_id = ? AND nid = ?", flowID, p.NetworkID(ctx)).Delete(&code.RegistrationCode{}) +} diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go index 9892f41e9fc0..b2f19f94726b 100644 --- a/persistence/sql/persister_verification.go +++ b/persistence/sql/persister_verification.go @@ -5,13 +5,11 @@ package sql import ( "context" - "crypto/subtle" "fmt" "time" "github.com/pkg/errors" - "github.com/ory/herodot" "github.com/ory/kratos/identity" "github.com/ory/kratos/persistence/sql/update" @@ -21,7 +19,6 @@ import ( "github.com/ory/x/sqlcon" "github.com/ory/kratos/selfservice/flow/verification" - "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/selfservice/strategy/link" ) @@ -137,154 +134,3 @@ func (p *Persister) DeleteExpiredVerificationFlows(ctx context.Context, expiresA } return nil } -func (p *Persister) UseVerificationCode(ctx context.Context, fID uuid.UUID, codeVal string) (*code.VerificationCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseVerificationCode") - defer span.End() - - var verificationCode *code.VerificationCode - - nid := p.NetworkID(ctx) - - flowTableName := new(verification.Flow).TableName(ctx) - - if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { - - if err := sqlcon.HandleError( - tx.RawQuery( - //#nosec G201 -- TableName is static - fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), - fID, - nid, - ).Exec(), - ); err != nil { - return err - } - - var submitCount int - // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. - if err := sqlcon.HandleError( - tx.RawQuery( - //#nosec G201 -- TableName is static - fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), - fID, - nid, - ).First(&submitCount), - ); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - // This check prevents parallel brute force attacks to generate the verification code - // by checking the submit count inside this database transaction. - // If the flow has been submitted more than 5 times, the transaction is aborted (regardless of whether the code was correct or not) - // and we thus give no indication whether the supplied code was correct or not. See also https://github.com/ory/kratos/pull/2645#discussion_r984732899 - if submitCount > 5 { - return errors.WithStack(code.ErrCodeSubmittedTooOften) - } - - var verificationCodes []code.VerificationCode - if err = sqlcon.HandleError( - tx.Where("nid = ? AND selfservice_verification_flow_id = ?", nid, fID). - All(&verificationCodes), - ); err != nil { - if errors.Is(err, sqlcon.ErrNoRows) { - // Return no error, as that would roll back the transaction - return nil - } - - return err - } - - secrets: - for _, secret := range p.r.Config().SecretsSession(ctx) { - suppliedCode := []byte(p.hmacValueWithSecret(ctx, codeVal, secret)) - for i := range verificationCodes { - code := verificationCodes[i] - if subtle.ConstantTimeCompare([]byte(code.CodeHMAC), suppliedCode) == 0 { - // Not the supplied code - continue - } - verificationCode = &code - break secrets - } - } - - if verificationCode == nil || verificationCode.Validate() != nil { - // Return no error, as that would roll back the transaction - return nil - } - - var va identity.VerifiableAddress - if err := tx.Where("id = ? AND nid = ?", verificationCode.VerifiableAddressID, nid).First(&va); err != nil { - return sqlcon.HandleError(err) - } - - verificationCode.VerifiableAddress = &va - - //#nosec G201 -- TableName is static - return tx. - RawQuery( - fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", verificationCode.TableName(ctx)), - time.Now().UTC(), - verificationCode.ID, - nid, - ).Exec() - })); err != nil { - return nil, err - } - - if verificationCode == nil { - return nil, errors.WithStack(code.ErrCodeNotFound) - } - - return verificationCode, nil -} - -func (p *Persister) CreateVerificationCode(ctx context.Context, c *code.CreateVerificationCodeParams) (*code.VerificationCode, error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateVerificationCode") - defer span.End() - - now := time.Now().UTC() - - verificationCode := &code.VerificationCode{ - ID: uuid.Nil, - CodeHMAC: p.hmacValue(ctx, c.RawCode), - ExpiresAt: now.Add(c.ExpiresIn), - IssuedAt: now, - FlowID: c.FlowID, - NID: p.NetworkID(ctx), - } - - if c.VerifiableAddress == nil { - return nil, errors.WithStack(herodot.ErrNotFound.WithReason("can't create a verification code without a verifiable address")) - } - - verificationCode.VerifiableAddress = c.VerifiableAddress - verificationCode.VerifiableAddressID = uuid.NullUUID{ - UUID: c.VerifiableAddress.ID, - Valid: true, - } - - // This should not create the request eagerly because otherwise we might accidentally create an address that isn't - // supposed to be in the database. - if err := p.GetConnection(ctx).Create(verificationCode); err != nil { - return nil, err - } - return verificationCode, nil -} - -func (p *Persister) DeleteVerificationCodesOfFlow(ctx context.Context, fID uuid.UUID) error { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteVerificationCodesOfFlow") - defer span.End() - - //#nosec G201 -- TableName is static - return p.GetConnection(ctx). - RawQuery( - fmt.Sprintf("DELETE FROM %s WHERE selfservice_verification_flow_id = ? AND nid = ?", new(code.VerificationCode).TableName(ctx)), - fID, - p.NetworkID(ctx), - ).Exec() -} diff --git a/persistence/sql/persister_verification_code.go b/persistence/sql/persister_verification_code.go new file mode 100644 index 000000000000..0b469bad07ce --- /dev/null +++ b/persistence/sql/persister_verification_code.go @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/x/sqlcon" +) + +func (p *Persister) CreateVerificationCode(ctx context.Context, params *code.CreateVerificationCodeParams) (*code.VerificationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateVerificationCode") + defer span.End() + + now := time.Now().UTC() + verificationCode := &code.VerificationCode{ + ID: uuid.Nil, + CodeHMAC: p.hmacValue(ctx, params.RawCode), + ExpiresAt: now.Add(params.ExpiresIn), + IssuedAt: now, + FlowID: params.FlowID, + NID: p.NetworkID(ctx), + } + + if params.VerifiableAddress == nil { + return nil, errors.WithStack(herodot.ErrNotFound.WithReason("can't create a verification code without a verifiable address")) + } + + verificationCode.VerifiableAddress = params.VerifiableAddress + verificationCode.VerifiableAddressID = uuid.NullUUID{ + UUID: params.VerifiableAddress.ID, + Valid: true, + } + + // This should not create the request eagerly because otherwise we might accidentally create an address that isn't + // supposed to be in the database. + if err := p.GetConnection(ctx).Create(verificationCode); err != nil { + return nil, err + } + + return verificationCode, nil +} + +func (p *Persister) UseVerificationCode(ctx context.Context, flowID uuid.UUID, userProvidedCode string) (*code.VerificationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseVerificationCode") + defer span.End() + + codeRow, err := useOneTimeCode[code.VerificationCode, *code.VerificationCode](ctx, p, flowID, userProvidedCode, new(verification.Flow).TableName(ctx), "selfservice_verification_flow_id") + if err != nil { + return nil, err + } + + var va identity.VerifiableAddress + if err := p.Connection(ctx).Where("id = ? AND nid = ?", codeRow.VerifiableAddressID, p.NetworkID(ctx)).First(&va); err != nil { + // This should fail on not found errors too, because the verifiable address must exist for the flow to work. + return nil, sqlcon.HandleError(err) + } + codeRow.VerifiableAddress = &va + + return codeRow, nil +} + +func (p *Persister) DeleteVerificationCodesOfFlow(ctx context.Context, fID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteVerificationCodesOfFlow") + defer span.End() + + return p.GetConnection(ctx).Where("selfservice_verification_flow_id = ? AND nid = ?", fID, p.NetworkID(ctx)).Delete(&code.VerificationCode{}) +} diff --git a/schema/errors.go b/schema/errors.go index 2c587e53dcd1..8cd74c5aec60 100644 --- a/schema/errors.go +++ b/schema/errors.go @@ -360,7 +360,7 @@ func NewNoCodeAuthnCredentials() error { func NewTraitsMismatch() error { return errors.WithStack(&ValidationError{ ValidationError: &jsonschema.ValidationError{ - Message: `the submitted form data has changed from the previous submission. Please try again.`, + Message: `the submitted form data has changed from the previous submission`, InstancePtr: "#/", }, Messages: new(text.Messages).Add(text.NewErrorValidationTraitsMismatch()), @@ -370,7 +370,7 @@ func NewTraitsMismatch() error { func NewRegistrationCodeInvalid() error { return errors.WithStack(&ValidationError{ ValidationError: &jsonschema.ValidationError{ - Message: `the provided code is invalid or has already been used. Please try again.`, + Message: `the provided code is invalid or has already been used`, InstancePtr: "#/", }, Messages: new(text.Messages).Add(text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed()), @@ -380,7 +380,7 @@ func NewRegistrationCodeInvalid() error { func NewLoginCodeInvalid() error { return errors.WithStack(&ValidationError{ ValidationError: &jsonschema.ValidationError{ - Message: `the provided code is invalid or has already been used. Please try again.`, + Message: `the provided code is invalid or has already been used`, InstancePtr: "#/", }, Messages: new(text.Messages).Add(text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed()), diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go index ef1f9ea3c617..6c7bc9709525 100644 --- a/selfservice/flow/request.go +++ b/selfservice/flow/request.go @@ -102,10 +102,7 @@ func MethodEnabledAndAllowedFromRequest(r *http.Request, flow FlowName, expected return MethodEnabledAndAllowed(r.Context(), flow, expected, method.Method, d) } -func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, actual string, d interface { - config.Provider -}, -) error { +func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, actual string, d config.Provider) error { if actual != expected { return errors.WithStack(ErrStrategyNotResponsible) } @@ -117,8 +114,6 @@ func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, a ok = d.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled case VerificationFlow, RecoveryFlow: ok = d.Config().SelfServiceCodeStrategy(ctx).Enabled - default: - ok = false } } else { ok = d.Config().SelfServiceStrategy(ctx, expected).Enabled diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go index 7c183413799d..689d52f0cb4f 100644 --- a/selfservice/strategy/code/code_login.go +++ b/selfservice/strategy/code/code_login.go @@ -8,6 +8,10 @@ import ( "database/sql" "time" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow" + "github.com/gofrs/uuid" "github.com/ory/kratos/identity" @@ -61,16 +65,25 @@ func (LoginCode) TableName(ctx context.Context) string { return "identity_login_codes" } -func (f LoginCode) IsExpired() bool { - return f.ExpiresAt.Before(time.Now()) +func (f *LoginCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } + if f.ExpiresAt.Before(time.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) + } + if f.UsedAt.Valid { + return errors.WithStack(ErrCodeAlreadyUsed) + } + return nil } -func (r LoginCode) WasUsed() bool { - return r.UsedAt.Valid +func (f *LoginCode) GetHMACCode() string { + return f.CodeHMAC } -func (f LoginCode) IsValid() bool { - return !f.IsExpired() && !f.WasUsed() +func (f *LoginCode) GetID() uuid.UUID { + return f.ID } // swagger:ignore diff --git a/selfservice/strategy/code/code_login_test.go b/selfservice/strategy/code/code_login_test.go new file mode 100644 index 000000000000..87b50155a15b --- /dev/null +++ b/selfservice/strategy/code/code_login_test.go @@ -0,0 +1,81 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestLoginCode(t *testing.T) { + conf, _ := internal.NewFastRegistryWithMocks(t) + + newCode := func(expiresIn time.Duration, f *login.Flow) *code.LoginCode { + return &code.LoginCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if flow is expired", func(t *testing.T) { + f, err := login.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := login.NewFlow(conf, time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := login.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := login.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.LoginCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) + }) + }) +} diff --git a/selfservice/strategy/code/code_recovery.go b/selfservice/strategy/code/code_recovery.go index f87a9c398de7..8d0cbe926063 100644 --- a/selfservice/strategy/code/code_recovery.go +++ b/selfservice/strategy/code/code_recovery.go @@ -8,6 +8,10 @@ import ( "database/sql" "time" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow" + "github.com/gofrs/uuid" "github.com/ory/herodot" @@ -73,16 +77,25 @@ func (RecoveryCode) TableName(ctx context.Context) string { return "identity_recovery_codes" } -func (f RecoveryCode) IsExpired() bool { - return f.ExpiresAt.Before(time.Now()) +func (f *RecoveryCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } + if f.ExpiresAt.Before(time.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) + } + if f.UsedAt.Valid { + return errors.WithStack(ErrCodeAlreadyUsed) + } + return nil } -func (r RecoveryCode) WasUsed() bool { - return r.UsedAt.Valid +func (f *RecoveryCode) GetHMACCode() string { + return f.CodeHMAC } -func (f RecoveryCode) IsValid() bool { - return !f.IsExpired() && !f.WasUsed() +func (f *RecoveryCode) GetID() uuid.UUID { + return f.ID } type CreateRecoveryCodeParams struct { diff --git a/selfservice/strategy/code/code_recovery_test.go b/selfservice/strategy/code/code_recovery_test.go index 3aadf350bb8c..dc099f02cae0 100644 --- a/selfservice/strategy/code/code_recovery_test.go +++ b/selfservice/strategy/code/code_recovery_test.go @@ -34,26 +34,26 @@ func TestRecoveryCode(t *testing.T) { } req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() - t.Run("method=IsExpired", func(t *testing.T) { - t.Run("case=returns true if flow is expired", func(t *testing.T) { + t.Run("case=returns error if flow is expired", func(t *testing.T) { f, err := recovery.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) c := newCode(-time.Hour, f) - require.True(t, c.IsExpired()) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) }) - t.Run("case=returns false if flow is not expired", func(t *testing.T) { + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { f, err := recovery.NewFlow(conf, time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) c := newCode(time.Hour, f) - require.False(t, c.IsExpired()) + require.NoError(t, c.Validate()) }) - }) - t.Run("method=WasUsed", func(t *testing.T) { - t.Run("case=returns true if flow has been used", func(t *testing.T) { + t.Run("case=returns error if flow has been used", func(t *testing.T) { f, err := recovery.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) @@ -62,9 +62,10 @@ func TestRecoveryCode(t *testing.T) { Time: time.Now(), Valid: true, } - require.True(t, c.WasUsed()) + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) }) - t.Run("case=returns false if flow has not been used", func(t *testing.T) { + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { f, err := recovery.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) require.NoError(t, err) @@ -72,7 +73,12 @@ func TestRecoveryCode(t *testing.T) { c.UsedAt = sql.NullTime{ Valid: false, } - require.False(t, c.WasUsed()) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.RecoveryCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) }) }) } diff --git a/selfservice/strategy/code/code_registration.go b/selfservice/strategy/code/code_registration.go index 256914760782..d8691b54b224 100644 --- a/selfservice/strategy/code/code_registration.go +++ b/selfservice/strategy/code/code_registration.go @@ -8,6 +8,10 @@ import ( "database/sql" "time" + "github.com/pkg/errors" + + "github.com/ory/kratos/selfservice/flow" + "github.com/gofrs/uuid" "github.com/ory/kratos/identity" @@ -60,16 +64,25 @@ func (RegistrationCode) TableName(ctx context.Context) string { return "identity_registration_codes" } -func (f RegistrationCode) IsExpired() bool { - return f.ExpiresAt.Before(time.Now()) +func (f *RegistrationCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } + if f.ExpiresAt.Before(time.Now().UTC()) { + return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) + } + if f.UsedAt.Valid { + return errors.WithStack(ErrCodeAlreadyUsed) + } + return nil } -func (r RegistrationCode) WasUsed() bool { - return r.UsedAt.Valid +func (f *RegistrationCode) GetHMACCode() string { + return f.CodeHMAC } -func (f RegistrationCode) IsValid() bool { - return !f.IsExpired() && !f.WasUsed() +func (f *RegistrationCode) GetID() uuid.UUID { + return f.ID } // swagger:ignore diff --git a/selfservice/strategy/code/code_registration_test.go b/selfservice/strategy/code/code_registration_test.go new file mode 100644 index 000000000000..034d9fcf2b92 --- /dev/null +++ b/selfservice/strategy/code/code_registration_test.go @@ -0,0 +1,80 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestRegistrationCode(t *testing.T) { + conf, _ := internal.NewFastRegistryWithMocks(t) + newCode := func(expiresIn time.Duration, f *registration.Flow) *code.RegistrationCode { + return &code.RegistrationCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if flow is expired", func(t *testing.T) { + f, err := registration.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := registration.NewFlow(conf, time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := registration.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := registration.NewFlow(conf, -time.Hour, "", req, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.RegistrationCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) + }) + }) +} diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index db3af496b59e..d3667aea3b07 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -72,6 +72,10 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit // send to all addresses for _, address := range addresses { + // We have to generate a unique code per address, or otherwise it is not possible to link which + // address was used to verify the code. + // + // See also [this discussion](https://github.com/ory/kratos/pull/3456#discussion_r1307560988). rawCode := GenerateCode() switch f.GetFlowName() { diff --git a/selfservice/strategy/code/verification_code.go b/selfservice/strategy/code/code_verification.go similarity index 93% rename from selfservice/strategy/code/verification_code.go rename to selfservice/strategy/code/code_verification.go index a4f204bae176..324766ebfa1c 100644 --- a/selfservice/strategy/code/verification_code.go +++ b/selfservice/strategy/code/code_verification.go @@ -62,6 +62,9 @@ func (VerificationCode) TableName(context.Context) string { // - If the code was already used `ErrCodeAlreadyUsed` is returnd // - Otherwise, `nil` is returned func (f *VerificationCode) Validate() error { + if f == nil { + return errors.WithStack(ErrCodeNotFound) + } if f.ExpiresAt.Before(time.Now().UTC()) { return errors.WithStack(flow.NewFlowExpiredError(f.ExpiresAt)) } @@ -71,6 +74,14 @@ func (f *VerificationCode) Validate() error { return nil } +func (f *VerificationCode) GetHMACCode() string { + return f.CodeHMAC +} + +func (f *VerificationCode) GetID() uuid.UUID { + return f.ID +} + type CreateVerificationCodeParams struct { // Code represents the recovery code RawCode string diff --git a/selfservice/strategy/code/code_verification_test.go b/selfservice/strategy/code/code_verification_test.go new file mode 100644 index 000000000000..3217b7dcbb00 --- /dev/null +++ b/selfservice/strategy/code/code_verification_test.go @@ -0,0 +1,81 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code_test + +import ( + "database/sql" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/x" + "github.com/ory/x/urlx" +) + +func TestVerificationCode(t *testing.T) { + conf, _ := internal.NewFastRegistryWithMocks(t) + + newCode := func(expiresIn time.Duration, f *verification.Flow) *code.VerificationCode { + return &code.VerificationCode{ + ID: x.NewUUID(), + FlowID: f.ID, + ExpiresAt: time.Now().Add(expiresIn), + } + } + + req := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} + t.Run("method=Validate", func(t *testing.T) { + t.Parallel() + + t.Run("case=returns error if flow is expired", func(t *testing.T) { + f, err := verification.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(-time.Hour, f) + expected := new(flow.ExpiredError) + require.ErrorAs(t, c.Validate(), &expected) + }) + t.Run("case=returns no error if flow is not expired", func(t *testing.T) { + f, err := verification.NewFlow(conf, time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow has been used", func(t *testing.T) { + f, err := verification.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + require.ErrorIs(t, c.Validate(), code.ErrCodeAlreadyUsed) + }) + + t.Run("case=returns no error if flow has not been used", func(t *testing.T) { + f, err := verification.NewFlow(conf, -time.Hour, "", req, nil, flow.TypeBrowser) + require.NoError(t, err) + + c := newCode(time.Hour, f) + c.UsedAt = sql.NullTime{ + Valid: false, + } + require.NoError(t, c.Validate()) + }) + + t.Run("case=returns error if flow is nil", func(t *testing.T) { + var c *code.VerificationCode + require.ErrorIs(t, c.Validate(), code.ErrCodeNotFound) + }) + }) +} diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index 032ed792a9e7..94c8de75e9b6 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -4,8 +4,6 @@ package code import ( - "context" - "encoding/json" "net/http" "strings" @@ -63,6 +61,7 @@ type ( x.CSRFTokenGeneratorProvider x.WriterProvider x.LoggingProvider + x.TracingProvider config.Provider @@ -113,6 +112,10 @@ type ( deps strategyDependencies dx *decoderx.HTTP } + + codeIdentifier struct { + Identifier string `json:"identifier"` + } ) func NewStrategy(deps strategyDependencies) *Strategy { @@ -294,169 +297,37 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { // 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 { +func (s *Strategy) NewCodeUINodes(r *http.Request, f flow.Flow, data any) 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 + prefix := "" // The login flow does not process traits 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()) - } + // The registration form does however + prefix = "traits" } - return nil -} - -type ( - FlowType interface { - *login.Flow | *registration.Flow | *recovery.Flow | *verification.Flow - flow.Flow - } - FlowPayload interface { - *updateLoginFlowWithCodeMethod | *updateRegistrationFlowWithCodeMethod | *updateRecoveryFlowWithCodeMethod | *updateVerificationFlowWithCodeMethod - } - CreateCodeState[T FlowType, P FlowPayload] func(context.Context, T, *Strategy, P) error - ValidateCodeState[T FlowType, P FlowPayload] func(context.Context, T, *Strategy, P) error - AlreadyValidatedCodeState[T FlowType, P FlowPayload] func(context.Context, T, *Strategy, P) error - CodeStateManager[T FlowType, P FlowPayload] struct { - f T - payload P - strategy *Strategy - createCodeState CreateCodeState[T, P] - verifyCodeState ValidateCodeState[T, P] - alreadyValidatedCodeState AlreadyValidatedCodeState[T, P] + cont, err := container.NewFromStruct("", node.CodeGroup, data, prefix) + if err != nil { + return err } -) -func NewCodeStateManager[T FlowType, P FlowPayload](f T, s *Strategy, payload P) *CodeStateManager[T, P] { - return &CodeStateManager[T, P]{ - f: f, - strategy: s, - payload: payload, + for _, n := range cont.Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.GetUI().GetNodes().SetValueAttribute(n.ID(), n.Attributes.GetValue()) } -} - -func (c *CodeStateManager[T, P]) SetCreateCodeHandler(fn CreateCodeState[T, P]) { - c.createCodeState = fn -} -func (c *CodeStateManager[T, P]) SetCodeVerifyHandler(fn ValidateCodeState[T, P]) { - c.verifyCodeState = fn -} - -func (c *CodeStateManager[T, P]) SetCodeDoneHandler(fn AlreadyValidatedCodeState[T, P]) { - c.alreadyValidatedCodeState = fn -} - -func (c *CodeStateManager[T, P]) validatePayload(ctx context.Context) error { - switch v := any(c.payload).(type) { - case *updateLoginFlowWithCodeMethod: - if len(v.Identifier) == 0 { - return errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) - } - case *updateRegistrationFlowWithCodeMethod: - if len(v.Traits) == 0 { - return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) - } - case *updateRecoveryFlowWithCodeMethod: - if len(v.Email) == 0 { - return errors.WithStack(schema.NewRequiredError("#/email", "email")) - } - case *updateVerificationFlowWithCodeMethod: - if len(v.Email) == 0 { - return errors.WithStack(schema.NewRequiredError("#/email", "email")) - } - default: - return errors.WithStack(herodot.ErrBadRequest.WithReason("received unexpected flow payload type")) - } return nil } -func (c *CodeStateManager[T, P]) getResend() string { - switch v := any(c.payload).(type) { - case *updateLoginFlowWithCodeMethod: - return v.Resend - case *updateRegistrationFlowWithCodeMethod: - return v.Resend - } - return "" -} - -func (c *CodeStateManager[T, P]) getCode() string { - switch v := any(c.payload).(type) { - case *updateLoginFlowWithCodeMethod: - return v.Code - case *updateRegistrationFlowWithCodeMethod: - return v.Code - case *updateRecoveryFlowWithCodeMethod: - return v.Code - case *updateVerificationFlowWithCodeMethod: - return v.Code - } - return "" -} - -func (c *CodeStateManager[T, P]) Run(ctx context.Context) error { +func SetDefaultFlowState(f flow.Flow, resend string) { // By Default the flow should be in the 'choose method' state. - if c.f.GetState() == "" { - c.f.SetState(flow.StateChooseMethod) - } - - if strings.EqualFold(c.getResend(), "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.f, c.strategy, 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.getCode()) == 0 { - return errors.WithStack(schema.NewRequiredError("#/code", "code")) - } - - if err := c.validatePayload(ctx); err != nil { - return err - } - - if err := c.verifyCodeState(ctx, c.f, c.strategy, c.payload); err != nil { - return err - } - case flow.StatePassedChallenge: - return c.alreadyValidatedCodeState(ctx, c.f, c.strategy, c.payload) - default: - return errors.WithStack(errors.New("Unknown flow state")) + if f.GetState() == "" { + f.SetState(flow.StateChooseMethod) } - 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: + if strings.EqualFold(resend, "code") { f.SetState(flow.StateChooseMethod) } } diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index 9345c834590a..264df98e2dd9 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -5,7 +5,6 @@ package code import ( "context" - "encoding/json" "net/http" "strings" @@ -13,6 +12,8 @@ import ( "github.com/pkg/errors" "github.com/ory/herodot" + "github.com/ory/x/otelx" + "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" @@ -64,15 +65,19 @@ func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.Au } } -func (s *Strategy) HandleLoginError(w http.ResponseWriter, r *http.Request, flow *login.Flow, body *updateLoginFlowWithCodeMethod, err error) error { - if flow != nil { +func (s *Strategy) HandleLoginError(r *http.Request, f *login.Flow, body *updateLoginFlowWithCodeMethod, err error) error { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } + + if f != nil { email := "" if body != nil { email = body.Identifier } - flow.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - flow.UI.GetNodes().Upsert( + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + f.UI.GetNodes().Upsert( node.NewInputField("identifier", email, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelID()), ) @@ -85,34 +90,25 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au 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) +func (s *Strategy) getIdentity(ctx context.Context, identifier string) (_ *identity.Identity, _ *identity.Credentials, err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.getIdentity") + defer otelx.End(span, &err) + + i, cred, err := s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identifier) if err != nil { return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) } - if err := s.deps.IdentityValidator().Validate(ctx, i); err != nil { - return nil, nil, errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) - } - - cred, ok := i.GetCredentials(s.ID()) - if !ok { - return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) - } else if len(cred.Identifiers) == 0 { + 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) { - s.deps.Audit(). - WithRequest(r). - WithField("identity_id", identityID). - WithField("login_flow_id", f.ID). - Info("Login with the code strategy started.") +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ uuid.UUID) (_ *identity.Identity, err error) { + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Login") + defer otelx.End(span, &err) if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { return nil, err @@ -129,157 +125,154 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, decoderx.MustHTTPRawJSONSchemaCompiler(loginMethodSchema), decoderx.HTTPDecoderAllowedMethods("POST"), decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { - return nil, s.HandleLoginError(w, r, f, &p, err) + return nil, s.HandleLoginError(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 nil, s.HandleLoginError(w, r, f, &p, err) + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.HandleLoginError(r, f, &p, err) } - codeManager := NewCodeStateManager(f, s, &p) + // By Default the flow should be in the 'choose method' state. + SetDefaultFlowState(f, p.Resend) - codeManager.SetCreateCodeHandler(func(ctx context.Context, f *login.Flow, strategy *Strategy, p *updateLoginFlowWithCodeMethod) error { - strategy.deps.Audit(). - WithSensitiveField("identifier", p.Identifier). - Info("Creating login code state.") - - // Step 1: Get the identity - i, cred, err := strategy.getIdentity(ctx, p.Identifier) + switch f.GetState() { + case flow.StateChooseMethod: + if err := s.loginSendEmail(ctx, w, r, f, &p); err != nil { + return nil, s.HandleLoginError(r, f, &p, err) + } + return nil, nil + case flow.StateEmailSent: + i, err := s.loginVerifyCode(ctx, r, f, &p) if err != nil { - return err + return nil, s.HandleLoginError(r, f, &p, err) } + return i, nil + case flow.StatePassedChallenge: + return nil, s.HandleLoginError(r, f, &p, errors.WithStack(schema.NewNoLoginStrategyResponsible())) + } - // Step 2: Delete any previous login codes for this flow ID - if err := strategy.deps.LoginCodePersister().DeleteLoginCodesOfFlow(ctx, f.GetID()); err != nil { - return errors.WithStack(err) - } + return nil, s.HandleLoginError(r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unexpected flow state: %s", f.GetState()))) +} - var identifier string - for _, id := range cred.Identifiers { - if strings.EqualFold(p.Identifier, id) { - identifier = id - } - } +func (s *Strategy) loginSendEmail(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginSendEmail") + defer otelx.End(span, &err) - addresses := []Address{ - { - To: identifier, - Via: identity.CodeAddressType(cred.IdentifierAddressType), - }, - } + if len(p.Identifier) == 0 { + return errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) + } - // kratos only supports `email` identifiers at the moment with the code method - // this is validated in the identity validation step above - if err := strategy.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { - return errors.WithStack(err) - } + p.Identifier = maybeNormalizeEmail(p.Identifier) - // sets the flow state to code sent - s.NextFlowState(f) + // Step 1: Get the identity + i, _, err := s.getIdentity(ctx, p.Identifier) + if err != nil { + return err + } - nodeData, err := json.Marshal(struct { - Identifier string `json:"identifier"` - }{ - Identifier: p.Identifier, - }) - if err != nil { - return errors.WithStack(err) - } + // Step 2: Delete any previous login codes for this flow ID + if err := s.deps.LoginCodePersister().DeleteLoginCodesOfFlow(ctx, f.GetID()); err != nil { + return errors.WithStack(err) + } - if err := s.NewCodeUINodes(r, f, nodeData); err != nil { - return err - } + addresses := []Address{{ + To: p.Identifier, + Via: identity.CodeAddressType(identity.AddressTypeEmail), + }} - f.Active = identity.CredentialsTypeCodeAuth - if err = strategy.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return err - } + // kratos only supports `email` identifiers at the moment with the code method + // this is validated in the identity validation step above + if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { + return errors.WithStack(err) + } - if x.IsJSONRequest(r) { - strategy.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) - } else { - http.Redirect(w, r, f.AppendTo(strategy.deps.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) - } + // sets the flow state to code sent + f.SetState(flow.NextState(f.GetState())) + + if err := s.NewCodeUINodes(r, f, &codeIdentifier{Identifier: p.Identifier}); err != nil { + return err + } - // 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) - }) + f.Active = identity.CredentialsTypeCodeAuth + if err = s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return err + } - codeManager.SetCodeVerifyHandler(func(ctx context.Context, f *login.Flow, strategy *Strategy, p *updateLoginFlowWithCodeMethod) error { - strategy.deps.Audit(). - WithSensitiveField("code", p.Code). - WithSensitiveField("identifier", p.Identifier). - Debug("Verifying login code") + if x.IsJSONRequest(r) { + s.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) + } - // Step 1: Get the identity - i, _, err = strategy.getIdentity(ctx, p.Identifier) - if err != nil { - return err - } + // 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) +} - loginCode, err := strategy.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) - } +// If identifier is an email, we lower case it because on mobile phones the first letter sometimes is capitalized. +func maybeNormalizeEmail(input string) string { + if strings.Contains(input, "@") { + return strings.ToLower(input) + } + return input +} - i, err = strategy.deps.PrivilegedIdentityPool().GetIdentity(ctx, loginCode.IdentityID, identity.ExpandDefault) - if err != nil { - return errors.WithStack(err) - } +func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod) (_ *identity.Identity, err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginVerifyCode") + defer otelx.End(span, &err) + + // we are in the second submission state of the flow + // we need to check the code and update the identity + if p.Code == "" { + return nil, errors.WithStack(schema.NewRequiredError("#/code", "code")) + } - // Step 2: The code was correct - f.Active = identity.CredentialsTypeCodeAuth + if len(p.Identifier) == 0 { + return nil, errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) + } - // since nothing has errored yet, we can assume that the code is correct - // and we can update the login flow - strategy.NextFlowState(f) + p.Identifier = maybeNormalizeEmail(p.Identifier) - if err := strategy.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return errors.WithStack(err) - } + // Step 1: Get the identity + i, _, err := s.getIdentity(ctx, p.Identifier) + if err != nil { + return nil, 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 := strategy.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &va); err != nil { - return err - } - break - } + loginCode, err := s.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) + if err != nil { + if errors.Is(err, ErrCodeNotFound) { + return nil, schema.NewLoginCodeInvalid() } + return nil, errors.WithStack(err) + } - return nil - }) + i, err = s.deps.PrivilegedIdentityPool().GetIdentity(ctx, loginCode.IdentityID, identity.ExpandDefault) + if err != nil { + return nil, errors.WithStack(err) + } - codeManager.SetCodeDoneHandler(func(ctx context.Context, f *login.Flow, strategy *Strategy, p *updateLoginFlowWithCodeMethod) error { - strategy.deps.Audit(). - WithSensitiveField("identifier", p.Identifier). - Debug("The login flow has already been completed, but is being re-requested.") - return errors.WithStack(schema.NewNoLoginStrategyResponsible()) - }) + // Step 2: The code was correct + f.Active = identity.CredentialsTypeCodeAuth - 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) + // since nothing has errored yet, we can assume that the code is correct + // and we can update the login flow + f.SetState(flow.NextState(f.GetState())) + + if err := s.deps.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, errors.WithStack(err) } - // 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")) + for idx := range i.VerifiableAddresses { + va := i.VerifiableAddresses[idx] + if !va.Verified && loginCode.Address == va.Value { + va.Verified = true + va.Status = identity.VerifiableAddressStatusCompleted + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, &va); err != nil { + return nil, err + } + break + } } return i, nil diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index 3f4a631a9e62..a371fdfbf5f6 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -12,6 +12,8 @@ import ( "net/url" "testing" + "github.com/ory/x/stringsx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -162,6 +164,27 @@ func TestLoginCodeStrategy(t *testing.T) { }, } { t.Run("test="+tc.d, func(t *testing.T) { + t.Run("case=email identifier should be case insensitive", func(t *testing.T) { + // create login flow + s := createLoginFlow(ctx, t, public, tc.isSPA) + + // submit email + s = submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("identifier", stringsx.ToUpperInitial(s.identityEmail)) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + submitLogin(ctx, t, s, tc.isSPA, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + }) + t.Run("case=should be able to log in with code", func(t *testing.T) { // create login flow s := createLoginFlow(ctx, t, public, tc.isSPA) @@ -331,7 +354,7 @@ func TestLoginCodeStrategy(t *testing.T) { }) }) - t.Run("case=resend code shoud invalidate previous code", func(t *testing.T) { + t.Run("case=resend code should invalidate previous code", func(t *testing.T) { ctx := context.Background() s := createLoginFlow(ctx, t, public, tc.isSPA) diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index dc4dce7a6b8d..f4c97641f055 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -10,6 +10,9 @@ import ( "net/http" "strings" + "github.com/ory/herodot" + "github.com/ory/x/otelx" + "github.com/pkg/errors" "github.com/ory/kratos/identity" @@ -59,19 +62,27 @@ type updateRegistrationFlowWithCodeMethod struct { Resend string `json:"resend" form:"resend"` } +func (p *updateRegistrationFlowWithCodeMethod) GetResend() string { + return p.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 { +func (s *Strategy) HandleRegistrationError(ctx context.Context, r *http.Request, f *registration.Flow, body *updateRegistrationFlowWithCodeMethod, err error) error { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return err + } + + if f != nil { if body != nil { - action := flow.AppendTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), registration.RouteSubmitFlow)).String() + action := f.AppendTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(ctx), registration.RouteSubmitFlow)).String() for _, n := range container.NewFromJSON(action, node.CodeGroup, body.Traits, "traits").Nodes { // we only set the value and not the whole field because we want to keep types from the initial form generation - flow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) } } - flow.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) } return err @@ -129,11 +140,9 @@ func (s *Strategy) getCredentialsFromTraits(ctx context.Context, f *registration return cred, nil } -func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) error { - s.deps.Audit(). - WithRequest(r). - WithField("registration_flow_id", f.ID). - Info("Registration with the code strategy started.") +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Register") + defer otelx.End(span, &err) if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { return err @@ -141,133 +150,135 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat 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) + return s.HandleRegistrationError(ctx, 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) + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { + return s.HandleRegistrationError(ctx, r, f, &p, err) } - codeManager := NewCodeStateManager(f, s, &p) + // By Default the flow should be in the 'choose method' state. + SetDefaultFlowState(f, p.Resend) - codeManager.SetCreateCodeHandler(func(ctx context.Context, f *registration.Flow, strategy *Strategy, p *updateRegistrationFlowWithCodeMethod) error { - strategy.deps.Logger(). - WithSensitiveField("traits", p.Traits). - WithSensitiveField("transient_paylaod", p.TransientPayload). - Debug("Creating registration code.") + switch f.GetState() { + case flow.StateChooseMethod: + return s.HandleRegistrationError(ctx, r, f, &p, s.registrationSendEmail(ctx, w, r, f, &p, i)) + case flow.StateEmailSent: + return s.HandleRegistrationError(ctx, r, f, &p, s.registrationVerifyCode(ctx, f, &p, i)) + case flow.StatePassedChallenge: + return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(schema.NewNoRegistrationStrategyResponsible())) + } - // Create the Registration code + return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unexpected flow state: %s", f.GetState()))) +} - // Step 1: validate the identity's traits - cred, err := strategy.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) - if err != nil { - return err - } +func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.registrationSendEmail") + defer otelx.End(span, &err) - // Step 2: Delete any previous registration codes for this flow ID - if err := strategy.deps.RegistrationCodePersister().DeleteRegistrationCodesOfFlow(ctx, f.ID); err != nil { - return errors.WithStack(err) - } + if len(p.Traits) == 0 { + return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) + } - // 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 := strategy.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { - return errors.WithStack(err) - } + // Create the Registration code - // sets the flow state to code sent - strategy.NextFlowState(f) + // Step 1: validate the identity's traits + cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + if err != nil { + return err + } - // 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 := strategy.NewCodeUINodes(r, f, p.Traits); err != nil { - return errors.WithStack(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) + } - f.Active = identity.CredentialsTypeCodeAuth - if err := strategy.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { - return errors.WithStack(err) - } + // Step 3: Get the identity email and send the code + var addresses []Address + for _, identifier := range cred.Identifiers { + addresses = append(addresses, Address{To: identifier, Via: identity.AddressTypeEmail}) + } + // kratos only supports `email` identifiers at the moment with the code method + // this is validated in the identity validation step above + if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { + return errors.WithStack(err) + } - if x.IsJSONRequest(r) { - strategy.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) - } else { - http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther) - } + // sets the flow state to code sent + f.SetState(flow.NextState(f.GetState())) - // 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, f *registration.Flow, strategy *Strategy, p *updateRegistrationFlowWithCodeMethod) error { - strategy.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 := strategy.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) - if err != nil { - return err - } + // 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) + } - // 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()) - } - } + f.Active = identity.CredentialsTypeCodeAuth + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } - // Step 3: Attempt to use the code - registrationCode, err := strategy.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) - } + if x.IsJSONRequest(r) { + s.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther) + } - // Step 4: The code was correct, populate the Identity credentials and traits - if err := strategy.handleIdentityTraits(ctx, f, p.Traits, p.TransientPayload, i, WithCredentials(registrationCode.AddressType, registrationCode.UsedAt)); err != nil { - return errors.WithStack(err) - } + // 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) + +} - // since nothing has errored yet, we can assume that the code is correct - // and we can update the registration flow - strategy.NextFlowState(f) +func (s *Strategy) registrationVerifyCode(ctx context.Context, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.registrationVerifyCode") + defer otelx.End(span, &err) - if err := strategy.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { - return errors.WithStack(err) - } + if len(p.Code) == 0 { + return errors.WithStack(schema.NewRequiredError("#/code", "code")) + } - return nil - }) + if len(p.Traits) == 0 { + return errors.WithStack(schema.NewRequiredError("#/traits", "traits")) + } - codeManager.SetCodeDoneHandler(func(ctx context.Context, f *registration.Flow, strategy *Strategy, p *updateRegistrationFlowWithCodeMethod) error { - strategy.deps.Audit(). - WithSensitiveField("traits", p.Traits). - WithSensitiveField("transient_payload", p.TransientPayload). - WithSensitiveField("code", p.Code). - Debug("The registration flow has already been completed, but is being re-requested.") + // 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 + } - return errors.WithStack(schema.NewNoRegistrationStrategyResponsible()) - }) + // 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()) + } + } - if err := codeManager.Run(r.Context()); err != nil { - if errors.Is(err, flow.ErrCompletedByStrategy) { - return err + // 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 s.HandleRegistrationError(w, r, f, &p, err) + return errors.WithStack(err) } + + // Step 4: The code was correct, populate the Identity credentials and traits + if err := s.handleIdentityTraits(ctx, f, p.Traits, p.TransientPayload, i, WithCredentials(registrationCode.AddressType, registrationCode.UsedAt)); err != nil { + return errors.WithStack(err) + } + + // since nothing has errored yet, we can assume that the code is correct + // and we can update the registration flow + f.SetState(flow.NextState(f.GetState())) + + if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil { + return errors.WithStack(err) + } + return nil } diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index edac27c8506c..d787b2b3fa4e 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -32,10 +32,11 @@ import ( ) type state struct { - flowID string - client *http.Client - email string - testServer *httptest.Server + flowID string + client *http.Client + email string + testServer *httptest.Server + resultIdentity *identity.Identity } func TestRegistrationCodeStrategyDisabled(t *testing.T) { @@ -130,15 +131,16 @@ func TestRegistrationCodeStrategy(t *testing.T) { registerNewUser := func(ctx context.Context, t *testing.T, s *state, isSPA bool, submitAssertion onSubmitAssertion) *state { t.Helper() - email := testhelpers.RandomEmail() - s.email = email + if s.email == "" { + s.email = testhelpers.RandomEmail() + } rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) values := testhelpers.SDKFormFieldsToURLValues(rf.Ui.Nodes) - values.Set("traits.email", email) + values.Set("traits.email", s.email) values.Set("method", "code") body, resp := testhelpers.RegistrationMakeRequest(t, false, isSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, false, values)) @@ -155,7 +157,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { } csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String() assert.NotEmptyf(t, csrfToken, "%s", body) - require.Equal(t, email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) + require.Equal(t, s.email, gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).attributes.value").String()) return s } @@ -186,7 +188,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { verifiableAddress, err := reg.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, s.email) require.NoError(t, err) - require.Equal(t, s.email, verifiableAddress.Value) + require.Equal(t, strings.ToLower(s.email), verifiableAddress.Value) id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, verifiableAddress.IdentityID) require.NoError(t, err) @@ -195,6 +197,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { _, ok := id.GetCredentials(identity.CredentialsTypeCodeAuth) require.True(t, ok) + s.resultIdentity = id return s } @@ -239,6 +242,35 @@ func TestRegistrationCodeStrategy(t *testing.T) { }, tc.isSPA, nil) }) + t.Run("case=should normalize email address on sign up", func(t *testing.T) { + ctx := context.Background() + + // 1. Initiate flow + state := createRegistrationFlow(ctx, t, public, tc.isSPA) + sourceMail := testhelpers.RandomEmail() + state.email = strings.ToUpper(sourceMail) + assert.NotEqual(t, sourceMail, state.email) + + // 2. Submit Identifier (email) + state = registerNewUser(ctx, t, state, tc.isSPA, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, sourceMail, "Complete your account registration") + assert.Contains(t, message.Body, "please complete your account registration by entering the following code") + + registrationCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, registrationCode) + + // 3. Submit OTP + state = submitOTP(ctx, t, reg, state, func(v *url.Values) { + v.Set("code", registrationCode) + }, tc.isSPA, nil) + + creds, ok := state.resultIdentity.GetCredentials(identity.CredentialsTypeCodeAuth) + require.True(t, ok) + require.Len(t, creds.Identifiers, 1) + assert.Equal(t, sourceMail, creds.Identifiers[0]) + }) + t.Run("case=should be able to resend the code", func(t *testing.T) { ctx := context.Background() diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index aea07b9f6ae9..c02e89105116 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -236,10 +236,6 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, f.Type, err) } - if err := code.Validate(); err != nil { - return s.retryVerificationFlowWithError(w, r, f.Type, err) - } - i, err := s.deps.IdentityPool().GetIdentity(r.Context(), code.VerifiableAddress.IdentityID, identity.ExpandDefault) if err != nil { return s.retryVerificationFlowWithError(w, r, f.Type, err) diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index dacdef8c9ff2..1587888e0168 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -50,8 +50,8 @@ func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { } func TestRegistration(t *testing.T) { - ctx := context.Background() + t.Run("case=registration", func(t *testing.T) { reg := newRegistrationRegistry(t) conf := reg.Config() diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts index 635e4415f4cb..9f6cdec24664 100644 --- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -20,9 +20,9 @@ context("Login error messages with code method", () => { ].forEach(({ route, profile, app }) => { describe(`for app ${app}`, () => { before(() => { - cy.proxy(app) cy.useConfigProfile(profile) cy.deleteMail() + cy.proxy(app) }) beforeEach(() => { @@ -123,8 +123,7 @@ context("Login error messages with code method", () => { it("should show error message when code is expired", () => { cy.updateConfigFile((config) => { config.selfservice.methods.code = { - registration_enabled: true, - login_enabled: true, + passwordless_enabled: true, config: { lifespan: "1ns", }, @@ -142,7 +141,7 @@ context("Login error messages with code method", () => { cy.url().should("contain", "login") cy.get("@email").then((email) => { - cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.getLoginCodeFromEmail(email.toString()).should((code) => { cy.get('input[name="code"]').type(code) }) }) @@ -164,8 +163,7 @@ context("Login error messages with code method", () => { cy.updateConfigFile((config) => { config.selfservice.methods.code = { - registration_enabled: true, - login_enabled: true, + passwordless_enabled: true, config: { lifespan: "1h", }, diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts index acbce5547899..8c0b3796461a 100644 --- a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -42,7 +42,7 @@ context("Login success with code method", () => { cy.get('input[name="identifier"]').clear().type(email.toString()) cy.submitCodeForm() - cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.getLoginCodeFromEmail(email.toString()).should((code) => { cy.get('input[name="code"]').type(code) cy.get("button[name=method][value=code]").click() @@ -68,13 +68,13 @@ context("Login success with code method", () => { cy.get('input[name="identifier"]').clear().type(email.toString()) cy.submitCodeForm() - cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.getLoginCodeFromEmail(email.toString()).should((code) => { cy.wrap(code).as("code1") }) cy.get("button[name=resend]").click() - cy.getLoginCodeFromEmail(email.toString()).then((code) => { + cy.getLoginCodeFromEmail(email.toString()).should((code) => { cy.wrap(code).as("code2") }) @@ -133,6 +133,9 @@ context("Login success with code method", () => { "traits.email2": email2, }, }) + + // There are verification emails from the registration process in the inbox that we need to deleted + // for the assertions below to pass. cy.deleteMail({ atLeast: 1 }) cy.visit(route) @@ -140,7 +143,7 @@ context("Login success with code method", () => { cy.get('input[name="identifier"]').clear().type(email2) cy.submitCodeForm() - cy.getLoginCodeFromEmail(email2).then((code) => { + cy.getLoginCodeFromEmail(email2).should((code) => { cy.get('input[name="code"]').type(code) cy.get("button[name=method][value=code]").click() }) diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts index 684019fa3cb7..a9b98f373e43 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -36,13 +36,12 @@ context("Registration error messages with code method", () => { cy.get('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(' input[name="code"]').type("invalid-code") + cy.get('input[name="code"]').type("invalid-code") cy.submitCodeForm() cy.get('[data-testid="ui/message/4040003"]').should( @@ -56,12 +55,15 @@ context("Registration error messages with code method", () => { cy.get('input[name="traits.email"]').type(email) cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) - cy.url().should("contain", "registration") cy.get('input[name="traits.email"]') .clear() .type("changed-email@email.com") - cy.get(' input[name="code"]').type("invalid-code") + cy.get('input[name="code"]').type("invalid-code") cy.submitCodeForm() cy.get('[data-testid="ui/message/4000030"]').should( @@ -75,12 +77,14 @@ context("Registration error messages with code method", () => { cy.get('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.removeAttribute(['input[name="code"]'], "required") - cy.submitCodeForm() + cy.submitCodeForm() cy.get('[data-testid="ui/message/4000002"]').should( "contain", "Property code is missing", @@ -102,14 +106,18 @@ context("Registration error messages with code method", () => { config.selfservice.methods.code.config.lifespan = "1ns" return config }) + cy.visit(route) const email = gen.email() + cy.get('input[name="traits.email"]').type(email) - cy.get(' input[name="traits.email"]').type(email) cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) - cy.url().should("contain", "registration") - cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.getRegistrationCodeFromEmail(email).should((code) => { cy.get('input[name="code"]').type(code) cy.submitCodeForm() }) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts index eaf734b144ed..299d5707a7d5 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -38,31 +38,33 @@ context("Registration success with code method", () => { it("should be able to resend the registration code", async () => { const email = gen.email() - cy.get(` input[name='traits.email']`).type(email) + cy.get(`input[name='traits.email']`).type(email) cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) - cy.url().should("contain", "registration") - - cy.getRegistrationCodeFromEmail(email).then((code) => + cy.getRegistrationCodeFromEmail(email).should((code) => cy.wrap(code).as("code1"), ) - cy.get(` input[name='traits.email']`).should("have.value", email) - cy.get(` input[name='method'][value='code'][type='hidden']`).should( + cy.get(`input[name='traits.email']`).should("have.value", email) + cy.get(`input[name='method'][value='code'][type='hidden']`).should( "exist", ) - cy.get(` button[name='resend'][value='code']`).click() + cy.get(`button[name='resend'][value='code']`).click() - cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.getRegistrationCodeFromEmail(email).should((code) => { cy.wrap(code).as("code2") }) cy.get("@code1").then((code1) => { // previous code should not work cy.get('input[name="code"]').clear().type(code1.toString()) - cy.submitCodeForm() + cy.submitCodeForm() cy.get('[data-testid="ui/message/4040003"]').should( "contain.text", "The registration code is invalid or has already been used. Please try again.", @@ -89,10 +91,13 @@ context("Registration success with code method", () => { cy.get(` input[name='traits.email']`).type(email) cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) - cy.url().should("contain", "registration") - cy.getRegistrationCodeFromEmail(email).then((code) => { - cy.get(` input[name=code]`).type(code) + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(`input[name=code]`).type(code) cy.get("button[name=method][value=code]").click() }) @@ -109,29 +114,28 @@ context("Registration success with code method", () => { cy.setPostCodeRegistrationHooks([]) const email = gen.email() - cy.get(` input[name='traits.email']`).type(email) + cy.get(`input[name='traits.email']`).type(email) cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) - cy.url().should("contain", "registration") - cy.getRegistrationCodeFromEmail(email).then((code) => { - cy.get(` input[name=code]`).type(code) + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(`input[name=code]`).type(code) cy.get("button[name=method][value=code]").click() }) - cy.deleteMail({ atLeast: 1 }) - cy.visit(login) - cy.get(` input[name=identifier]`).type(email) + cy.get(`input[name=identifier]`).type(email) cy.get("button[name=method][value=code]").click() cy.getLoginCodeFromEmail(email).then((code) => { - cy.get(`input[name = code]`).type(code) + cy.get(`input[name=code]`).type(code) cy.get("button[name=method][value=code]").click() }) - cy.deleteMail({ atLeast: 1 }) - cy.getSession().should((session) => { const { identity } = session expect(identity.id).to.not.be.empty @@ -179,35 +183,40 @@ context("Registration success with code method", () => { cy.get(`input[name='traits.username']`).type(Math.random().toString(36)) const email = gen.email() - - cy.get(` input[name='traits.email']`).type(email) + cy.get(`input[name='traits.email']`).type(email) const email2 = gen.email() - - cy.get(` input[name='traits.email2']`).type(email2) + cy.get(`input[name='traits.email2']`).type(email2) cy.submitCodeForm() + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) - // intentionally use email 1 to verify the account - cy.url().should("contain", "registration") - cy.getRegistrationCodeFromEmail(email, { expectedCount: 2 }).then( + // intentionally use email 1 to sign up for the account + cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).should( (code) => { cy.get(`input[name=code]`).type(code) cy.get("button[name=method][value=code]").click() }, ) - cy.deleteMail({ atLeast: 2 }) - cy.logout() + // There are verification emails from the registration process in the inbox that we need to deleted + // for the assertions below to pass. + cy.deleteMail({ atLeast: 1 }) + // Attempt to sign in with email 2 (should fail) cy.visit(login) - cy.get(` input[name=identifier]`).type(email2) + cy.get(`input[name=identifier]`).type(email2) cy.get("button[name=method][value=code]").click() - cy.getLoginCodeFromEmail(email2).then((code) => { + cy.getLoginCodeFromEmail(email2, { + expectedCount: 1, + }).should((code) => { cy.get(`input[name=code]`).type(code) cy.get("button[name=method][value=code]").click() }) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index b6b4226c6e46..d924827b5881 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -384,9 +384,13 @@ Cypress.Commands.add( Cypress.Commands.add( "registerWithCode", - ({ email = gen.email(), code = undefined, traits = {}, query = {} } = {}) => { - console.log("Creating user account: ", { email }) - + ({ + email = gen.email(), + code = undefined, + traits = {}, + query = {}, + expectedMailCount = 1, + } = {}) => { cy.clearAllCookies() cy.request({ @@ -394,7 +398,6 @@ Cypress.Commands.add( method: "GET", followRedirect: false, headers: { - "Content-Type": "application/json", Accept: "application/json", }, qs: query || {}, @@ -419,7 +422,6 @@ Cypress.Commands.add( }) .then(({ body }) => { if (!code) { - console.log("registration with code", body) expect( body.ui.nodes.find( (f: UiNode) => @@ -429,14 +431,9 @@ Cypress.Commands.add( ).attributes.value, ).to.eq(email) - const expectedCount = - Object.keys(traits) - .map((k) => (k.includes("email") ? k : null)) - .filter(Boolean).length + 1 - return cy .getRegistrationCodeFromEmail(email, { - expectedCount: expectedCount, + expectedCount: expectedMailCount, }) .then((code) => { return cy.request({ @@ -1190,7 +1187,7 @@ Cypress.Commands.add( Cypress.Commands.add( "verifyEmailButExpired", ({ expect: { email }, strategy = "code" }) => { - cy.getMail().then((message) => { + cy.getMail().should((message) => { expect(message.subject).to.equal("Please verify your email address") expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh") @@ -1263,28 +1260,32 @@ Cypress.Commands.add( const req = () => cy.request(`${MAIL_API}/mail`).then((response) => { expect(response.body).to.have.property("mailItems") - const count = response.body.mailItems.length + let 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) => + const filtered = response.body.mailItems.filter((m: any) => m.toAddresses.includes(email), ) - if (!mailItem) { - return req + + if (filtered.length === 0) { + tries++ + cy.wait(pollInterval) + return req() } + + expect(filtered.length).to.equal(expectedCount) + mailItem = filtered[0] } else { + expect(count).to.equal(expectedCount) 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) @@ -1485,7 +1486,7 @@ Cypress.Commands.add("getVerificationCodeFromEmail", (email) => { Cypress.Commands.add("enableRegistrationViaCode", (enable: boolean = true) => { cy.updateConfigFile((config) => { - config.selfservice.methods.code.registration_enabled = enable + config.selfservice.methods.code.passwordless_enabled = enable return config }) }) diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/cypress/support/config.d.ts index 762dbe4e090b..1bac43efd7b3 100644 --- a/test/e2e/cypress/support/config.d.ts +++ b/test/e2e/cypress/support/config.d.ts @@ -527,8 +527,7 @@ export interface OryKratosConfiguration2 { config?: LinkConfiguration } code?: { - login_enabled?: EnablesLoginWithCodeMethod - registration_enabled?: EnablesRegistrationWithCodeMethod + passwordless_enabled?: boolean enabled?: EnablesCodeMethod config?: CodeConfiguration } diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index a30442ee68af..47b66308c252 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -80,6 +80,7 @@ declare global { code?: string traits?: { [key: string]: any } query?: { [key: string]: string } + expectedMailCount?: number }): Chainable> /** @@ -731,7 +732,7 @@ declare global { */ getRegistrationCodeFromEmail( email: string, - opts?: { expectedCount: number }, + opts?: { expectedCount: number; removeMail?: boolean }, ): Chainable /** diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 05820e0002e9..ec69fb050fab 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -37,8 +37,7 @@ selfservice: password: enabled: false code: - registration_enabled: true - login_enabled: true + passwordless_enabled: true enabled: true config: lifespan: 1h From d4a25839c488126c7a5b1a51b2e88012fe082d94 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:26:44 +0200 Subject: [PATCH 22/24] chore: synchronize workspaces --- selfservice/flow/verification/handler_test.go | 4 ++-- .../cypress/integration/profiles/code/login/error.spec.ts | 4 ++-- .../cypress/integration/profiles/code/login/success.spec.ts | 5 ++--- .../integration/profiles/code/registration/error.spec.ts | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/selfservice/flow/verification/handler_test.go b/selfservice/flow/verification/handler_test.go index 4eac94367b59..33da0e2a204e 100644 --- a/selfservice/flow/verification/handler_test.go +++ b/selfservice/flow/verification/handler_test.go @@ -229,8 +229,8 @@ func TestPostFlow(t *testing.T) { IdentityID: uuid.NullUUID{UUID: uuid.Must(uuid.NewV4()), Valid: true}, AMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, }, - SessionID: uuid.NullUUID{UUID: s.ID, Valid: true}, - State: flow.StatePassedChallenge, + SessionID: uuid.NullUUID{UUID: s.ID, Valid: true}, + State: flow.StatePassedChallenge, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts index 9f6cdec24664..46d8854c4e99 100644 --- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -46,7 +46,7 @@ context("Login error messages with code method", () => { cy.url().should("contain", "login") - cy.get('[data-testid="ui/message/4000029"]').should( + cy.get('[data-testid="ui/message/4000035"]').should( "contain", "This account does not exist or has not setup sign in with code.", ) @@ -86,7 +86,7 @@ context("Login error messages with code method", () => { cy.get('input[name="code"]').type("invalid-code") cy.submitCodeForm() - cy.get('[data-testid="ui/message/4000029"]').should( + cy.get('[data-testid="ui/message/4000035"]').should( "contain", "This account does not exist or has not setup sign in with code.", ) diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts index 8c0b3796461a..83cbbb605c20 100644 --- a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -48,9 +48,8 @@ context("Login success with code method", () => { cy.get("button[name=method][value=code]").click() }) - if (app === "express") { - cy.get('a[href*="sessions"').click() - } + cy.location("pathname").should("not.contain", "login") + cy.getSession().should((session) => { const { identity } = session expect(identity.id).to.not.be.empty diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts index a9b98f373e43..a4d6596008dd 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -66,7 +66,7 @@ context("Registration error messages with code method", () => { cy.get('input[name="code"]').type("invalid-code") cy.submitCodeForm() - cy.get('[data-testid="ui/message/4000030"]').should( + cy.get('[data-testid="ui/message/4000036"]').should( "contain", "The provided traits do not match the traits previously associated with this flow.", ) From cdccbefd4db87463dd0388eb8d4fff1f4d7191d0 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:29:45 +0200 Subject: [PATCH 23/24] chore: synchronize workspaces --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2759e18aae2e..854832f11cb6 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ test-short: .PHONY: test-coverage test-coverage: .bin/go-acc .bin/goveralls - go-acc -o coverage.out ./... -- -v -failfast -timeout=20m -tags sqlite,json1 + go-acc -o coverage.out ./... -- -failfast -timeout=20m -tags sqlite,json1 .PHONY: test-coverage-next test-coverage-next: .bin/go-acc .bin/goveralls From 99922c175cd209dfdb4a2e468d9e0021d05c2d7d Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:41:28 +0200 Subject: [PATCH 24/24] chore: synchronize workspaces --- internal/client-go/model_identity_credentials_code.go | 2 +- internal/httpclient/model_identity_credentials_code.go | 2 +- selfservice/flow/verification/handler_test.go | 3 +-- spec/api.json | 2 +- spec/swagger.json | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/client-go/model_identity_credentials_code.go b/internal/client-go/model_identity_credentials_code.go index b3a2914dec14..f542b359639a 100644 --- a/internal/client-go/model_identity_credentials_code.go +++ b/internal/client-go/model_identity_credentials_code.go @@ -16,7 +16,7 @@ import ( "time" ) -// IdentityCredentialsCode CredentialsCode represents a one time login/registraiton code +// IdentityCredentialsCode CredentialsCode represents a one time login/registration code type IdentityCredentialsCode struct { AddressType *string `json:"address_type,omitempty"` UsedAt NullableTime `json:"used_at,omitempty"` diff --git a/internal/httpclient/model_identity_credentials_code.go b/internal/httpclient/model_identity_credentials_code.go index b3a2914dec14..f542b359639a 100644 --- a/internal/httpclient/model_identity_credentials_code.go +++ b/internal/httpclient/model_identity_credentials_code.go @@ -16,7 +16,7 @@ import ( "time" ) -// IdentityCredentialsCode CredentialsCode represents a one time login/registraiton code +// IdentityCredentialsCode CredentialsCode represents a one time login/registration code type IdentityCredentialsCode struct { AddressType *string `json:"address_type,omitempty"` UsedAt NullableTime `json:"used_at,omitempty"` diff --git a/selfservice/flow/verification/handler_test.go b/selfservice/flow/verification/handler_test.go index 33da0e2a204e..6eb2b12f800b 100644 --- a/selfservice/flow/verification/handler_test.go +++ b/selfservice/flow/verification/handler_test.go @@ -229,8 +229,7 @@ func TestPostFlow(t *testing.T) { IdentityID: uuid.NullUUID{UUID: uuid.Must(uuid.NewV4()), Valid: true}, AMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, }, - SessionID: uuid.NullUUID{UUID: s.ID, Valid: true}, - State: flow.StatePassedChallenge, + State: flow.StatePassedChallenge, } require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) diff --git a/spec/api.json b/spec/api.json index 596ab6ab9984..005fef8d643d 100644 --- a/spec/api.json +++ b/spec/api.json @@ -910,7 +910,7 @@ "type": "object" }, "identityCredentialsCode": { - "description": "CredentialsCode represents a one time login/registraiton code", + "description": "CredentialsCode represents a one time login/registration code", "properties": { "address_type": { "$ref": "#/components/schemas/CodeAddressType" diff --git a/spec/swagger.json b/spec/swagger.json index 63e280fc3da5..368785b299a8 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3865,7 +3865,7 @@ } }, "identityCredentialsCode": { - "description": "CredentialsCode represents a one time login/registraiton code", + "description": "CredentialsCode represents a one time login/registration code", "type": "object", "properties": { "address_type": {