// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package password_test import ( "context" _ "embed" "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "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/pkg" kratos "github.com/ory/kratos/pkg/httpclient" "github.com/ory/kratos/pkg/registrationhelpers" "github.com/ory/kratos/pkg/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "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/kratos/x/nosurfx" "github.com/ory/x/assertx" "github.com/ory/x/configx" "github.com/ory/x/snapshotx" "github.com/ory/x/sqlcon" ) var flows = []string{"spa", "api", "browser"} func newRegistrationRegistry(t *testing.T, cfgOpts ...configx.OptionModifier) *driver.RegistryDefault { _, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{ configx.WithValues(map[string]any{ config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, config.ViperKeySelfServiceRegistrationLoginHints: true, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, }), }, cfgOpts...)...) return reg } func TestRegistration(t *testing.T) { t.Parallel() t.Run("case=registration", func(t *testing.T) { t.Parallel() reg := newRegistrationRegistry(t, configx.WithValues(map[string]any{ config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, config.ViperKeyDefaultIdentitySchemaID: "default", config.ViperKeyIdentitySchemas: config.Schemas{ {ID: "default", URL: "file://stub/registration.schema.json", SelfserviceSelectable: true}, {ID: "other", URL: "file://stub/registration.schema.json", SelfserviceSelectable: true}, }, })) conf := reg.Config() publicTS, _ := testhelpers.NewKratosServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) redirNoSessionTS := testhelpers.NewRedirNoSessionTS(t, reg) // set the "return to" server, which will assert the session state // (redirTS: enforce that a session exists, redirNoSessionTS: enforce that no session exists) useReturnToFromTS := func(ts *httptest.Server) { conf.MustSet(t.Context(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to") conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts") } useReturnToFromTS(redirTS) apiClient := testhelpers.NewDebugClient(t) t.Run("AssertCommonErrorCases", func(t *testing.T) { registrationhelpers.AssertCommonErrorCases(t, flows) }) t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) { reg := newRegistrationRegistry(t) registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) { v.Del("traits.foobar") }) }) t.Run("AssertCSRFFailures", func(t *testing.T) { reg := newRegistrationRegistry(t) registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) { v.Set("password", x.NewUUID().String()) v.Set("method", identity.CredentialsTypePassword.String()) }) }) t.Run("AssertSchemaDoesNotExist", func(t *testing.T) { reg := newRegistrationRegistry(t) registrationhelpers.AssertSchemaDoesNotExist(t, reg, flows, func(v url.Values) { v.Set("password", x.NewUUID().String()) v.Set("method", identity.CredentialsTypePassword.String()) }) }) expectRegistrationBody := func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { if isAPI { return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values, isSPA, http.StatusOK, publicTS.URL+registration.RouteSubmitFlow) } if hc == nil { hc = testhelpers.NewClientWithCookies(t) } expectReturnTo := browserRedirTS.URL + "/registration-return-ts" if isSPA { expectReturnTo = publicTS.URL } return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values, isSPA, http.StatusOK, expectReturnTo) } expectSuccessfulRegistration := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirTS) return expectRegistrationBody(t, redirTS, isAPI, isSPA, hc, values) } expectNoRegistration := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirNoSessionTS) t.Cleanup(func() { useReturnToFromTS(redirTS) }) return expectRegistrationBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) } t.Run("case=should reject invalid transient payload", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) t.Cleanup(func() { conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) }) for _, f := range flows { t.Run("type="+f, func(t *testing.T) { username := x.NewUUID().String() body := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, func(v url.Values) { v.Set("traits.username", username) v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") v.Set("transient_payload", "42") }) assert.Equal(t, "bar", gjson.Get(body, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", body) assert.Equal(t, username, gjson.Get(body, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", body) assert.Equal(t, int64(4000026), gjson.Get(body, "ui.nodes.#(attributes.name==transient_payload).messages.0.id").Int(), "%s", body) }) } }) t.Run("case=should accept valid transient payload", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) t.Cleanup(func() { conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) }) setValues := func(username string, v url.Values) { v.Set("traits.username", username) v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") v.Set("transient_payload.stuff", "42") } t.Run("type=api", func(t *testing.T) { username := x.NewUUID().String() body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { username := x.NewUUID().String() body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { username := x.NewUUID().String() body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) }) }) t.Run("case=should pass and set up a session", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) t.Cleanup(func() { conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) }) t.Run("type=api", func(t *testing.T) { body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-8-api`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) assert.NotContains(t, gjson.Get(body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", body) }) t.Run("type=spa", func(t *testing.T) { body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-8-spa`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-8-browser`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) }) t.Run("case=should not set up a session if hook is not configured", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) t.Run("type=api", func(t *testing.T) { body := expectNoRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-8-api-nosession`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session.id").String(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { expectNoRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) }) t.Run("type=browser", func(t *testing.T) { expectNoRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) }) }) t.Run("case=should fail to register the same user again", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) t.Cleanup(func() { conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) }) applyTransform := func(values, transform func(v url.Values)) func(v url.Values) { return func(v url.Values) { values(v) transform(v) } } // test duplicate registration on all client types, where the values can be transformed before // they are sent the second time testWithTransform := func(t *testing.T, suffix string, transform func(v url.Values)) { t.Run("type=api", func(t *testing.T) { values := func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api-duplicate-"+suffix) v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") } _ = expectSuccessfulRegistration(t, true, false, apiClient, values) body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS, applyTransform(values, transform), false, http.StatusBadRequest, publicTS.URL+registration.RouteSubmitFlow) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-api-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) t.Run("type=spa", func(t *testing.T) { values := func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa-duplicate-"+suffix) v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") } _ = expectSuccessfulRegistration(t, false, true, nil, values) body := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, "spa", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-spa-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) t.Run("type=browser", func(t *testing.T) { values := func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser-duplicate-"+suffix) v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") } _ = expectSuccessfulRegistration(t, false, false, nil, values) body := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, "browser", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-browser-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) } t.Run("case=identical input", func(t *testing.T) { testWithTransform(t, "identical", func(v url.Values) { // base case }) }) t.Run("case=different capitalization", func(t *testing.T) { testWithTransform(t, "caps", func(v url.Values) { v.Set("traits.username", strings.ToUpper(v.Get("traits.username"))) }) }) t.Run("case=leading whitespace", func(t *testing.T) { testWithTransform(t, "leading", func(v url.Values) { v.Set("traits.username", " "+v.Get("traits.username")) }) }) t.Run("case=trailing whitespace", func(t *testing.T) { testWithTransform(t, "trailing", func(v url.Values) { v.Set("traits.username", v.Get("traits.username")+" ") }) }) }) t.Run("case=should return correct error ids from validation failures", func(t *testing.T) { test := func(t *testing.T, constraint string, setValues func(url.Values), expectedId text.ID, expectedMesage string) { template := `{ "$id": "https://example.com/person.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Person", "type": "object", "properties": { "traits": { "type": "object", "properties": { "foobar": { %s }, "username": { "type": "string", "ory.sh/kratos": { "credentials": { "password": { "identifier": true } } } } }, "required": [ "foobar", "username" ] } }, "additionalProperties": false }` testhelpers.SetDefaultIdentitySchemaFromRaw(conf, []byte(fmt.Sprintf(template, constraint))) browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) c := f.Ui values := testhelpers.SDKFormFieldsToURLValues(c.Nodes) setValues(values) values.Set("traits.username", "registration-identifier-9") values.Set("password", x.NewUUID().String()) actual, _ := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, values.Encode()) assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) registrationhelpers.CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username") assert.EqualValues(t, "registration-identifier-9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.messages").Array()) assert.Len(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages").Array(), 0) assert.Equal(t, int64(expectedId), gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.id").Int()) assert.Equal(t, expectedMesage, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.text").String()) assert.Equal(t, "error", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.type").String()) } const key = "traits.foobar" t.Run("case=string violating minLength", func(t *testing.T) { test(t, `"type": "string", "minLength": 5`, func(v url.Values) { v.Set(key, "bar") }, text.ErrorValidationMinLength, "length must be >= 5, but got 3") }) t.Run("case=string violating maxLength", func(t *testing.T) { test(t, `"type": "string", "maxLength": 5`, func(v url.Values) { v.Set(key, "qwerty") }, text.ErrorValidationMaxLength, "length must be <= 5, but got 6") }) t.Run("case=string violating pattern", func(t *testing.T) { test(t, `"type": "string", "pattern": "^[a-z]*$"`, func(v url.Values) { v.Set(key, "FUBAR") }, text.ErrorValidationInvalidFormat, "does not match pattern \"^[a-z]*$\"") }) t.Run("case=number violating minimum", func(t *testing.T) { test(t, `"type": "number", "minimum": 5`, func(v url.Values) { v.Set(key, "3") }, text.ErrorValidationMinimum, "must be >= 5 but found 3") }) t.Run("case=number violating exclusiveMinimum", func(t *testing.T) { test(t, `"type": "number", "exclusiveMinimum": 5`, func(v url.Values) { v.Set(key, "5") }, text.ErrorValidationExclusiveMinimum, "must be > 5 but found 5") }) t.Run("case=number violating maximum", func(t *testing.T) { test(t, `"type": "number", "maximum": 5`, func(v url.Values) { v.Set(key, "6") }, text.ErrorValidationMaximum, "must be <= 5 but found 6") }) t.Run("case=number violating exclusiveMaximum", func(t *testing.T) { test(t, `"type": "number", "exclusiveMaximum": 5`, func(v url.Values) { v.Set(key, "5") }, text.ErrorValidationExclusiveMaximum, "must be < 5 but found 5") }) t.Run("case=number violating multipleOf", func(t *testing.T) { test(t, `"type": "number", "multipleOf": 3`, func(v url.Values) { v.Set(key, "7") }, text.ErrorValidationMultipleOf, "7 not multipleOf 3") }) t.Run("case=array violating maxItems", func(t *testing.T) { test(t, `"type": "array", "items": { "type": "string" }, "maxItems": 3`, func(v url.Values) { v.Add(key, "a"); v.Add(key, "b"); v.Add(key, "c"); v.Add(key, "d") }, text.ErrorValidationMaxItems, "maximum 3 items allowed, but found 4 items") }) t.Run("case=array violating minItems", func(t *testing.T) { test(t, `"type": "array", "items": { "type": "string" }, "minItems": 3`, func(v url.Values) { v.Add(key, "a"); v.Add(key, "b") }, text.ErrorValidationMinItems, "minimum 3 items allowed, but found 2 items") }) t.Run("case=array violating uniqueItems", func(t *testing.T) { test(t, `"type": "array", "items": { "type": "string" }, "uniqueItems": true`, func(v url.Values) { v.Add(key, "abc"); v.Add(key, "XYZ"); v.Add(key, "abc") }, text.ErrorValidationUniqueItems, "items at index 0 and 2 are equal") }) t.Run("case=wrong type", func(t *testing.T) { test(t, `"type": "number"`, func(v url.Values) { v.Set(key, "blabla") }, text.ErrorValidationWrongType, "expected number, but got string") }) }) t.Run("case=should return an error because not passing validation and reset previous errors and values", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") check := func(t *testing.T, actual string) { assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) registrationhelpers.CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username") } checkFirst := func(t *testing.T, actual string) { check(t, actual) assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages.0").String(), `Property username is missing`, "%s", actual) } checkSecond := func(t *testing.T, actual string) { check(t, actual) assert.EqualValues(t, "registration-identifier-9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages").Array()) assert.Empty(t, gjson.Get(actual, "ui.nodes.messages").Array()) assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual) } valuesFirst := func(v url.Values) url.Values { v.Del("traits.username") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") return v } valuesSecond := func(v url.Values) url.Values { v.Set("traits.username", "registration-identifier-9") v.Set("password", x.NewUUID().String()) v.Del("traits.foobar") return v } t.Run("type=api", func(t *testing.T) { f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) c := f.Ui actual, _ := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, valuesFirst(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) checkFirst(t, actual) actual, _ = testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, valuesSecond(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) checkSecond(t, actual) }) t.Run("type=spa", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true, false, false) c := f.Ui actual, _ := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, testhelpers.EncodeFormAsJSON(t, false, valuesFirst(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) checkFirst(t, actual) actual, _ = testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, testhelpers.EncodeFormAsJSON(t, false, valuesSecond(testhelpers.SDKFormFieldsToURLValues(c.Nodes)))) checkSecond(t, actual) }) t.Run("type=browser", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) c := f.Ui actual, _ := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, valuesFirst(testhelpers.SDKFormFieldsToURLValues(c.Nodes)).Encode()) checkFirst(t, actual) actual, _ = testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, valuesSecond(testhelpers.SDKFormFieldsToURLValues(c.Nodes)).Encode()) checkSecond(t, actual) }) }) t.Run("case=should work even if password is just numbers", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://stub/registration.schema.json") conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) t.Cleanup(func() { conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) }) t.Run("type=api", func(t *testing.T) { actual := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-10-api`, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) }) t.Run("type=spa", func(t *testing.T) { actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-10-spa`, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) }) t.Run("type=browser", func(t *testing.T) { actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-10-browser`, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) }) }) t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json") for _, f := range []string{"spa", "api", "browser"} { t.Run("type="+f, func(t *testing.T) { actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, func(v url.Values) { v.Set("traits.email", testhelpers.RandomEmail()) v.Set("password", x.NewUUID().String()) v.Set("method", "password") }) assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) }) } }) t.Run("case=should work with regular JSON", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://stub/registration.schema.json") conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) t.Cleanup(func() { conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) }) hc := testhelpers.NewClientWithCookies(t) hc.Transport = testhelpers.NewTransportWithLogger(hc.Transport, t) payload := testhelpers.InitializeRegistrationFlowViaBrowser(t, hc, publicTS, false, false, false) values := testhelpers.SDKFormFieldsToURLValues(payload.Ui.Nodes) time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out. username := x.NewUUID() actual, res := testhelpers.RegistrationMakeRequest(t, true, false, payload, hc, fmt.Sprintf(`{ "method": "password", "csrf_token": "%s", "password": "%s", "traits": { "foobar": "bar", "username": "%s" } }`, values.Get("csrf_token"), x.NewUUID(), username)) assert.EqualValues(t, http.StatusOK, res.StatusCode, assertx.PrettifyJSONPayload(t, actual)) assert.Equal(t, username.String(), gjson.Get(actual, "identity.traits.username").String(), "%s", actual) }) t.Run("case=should choose the correct identity schema", func(t *testing.T) { conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "advanced-user") conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{ {ID: "does-not-exist", URL: "file://./stub/not-exists.schema.json"}, {ID: "advanced-user", URL: "file://./stub/registration.secondary.schema.json"}, }) conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) username := "registration-custom-schema" for _, tc := range []struct { name string isAPI bool isSPA bool }{ { name: "api", isAPI: true, }, { name: "spa", isSPA: true, }, { name: "browser", }, } { t.Run("type="+tc.name, func(t *testing.T) { body := expectNoRegistration(t, tc.isAPI, tc.isSPA, nil, func(v url.Values) { v.Set("traits.username", username+"-"+tc.name) v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") }) if tc.isAPI || tc.isSPA { // body is empty for browser flow assert.Equal(t, username+"-"+tc.name, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session.id").String(), "%s", body) } }) } }) t.Run("case=multi-schema select", func(t *testing.T) { conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{ {ID: "username", URL: "file://stub/sort.schema.json", SelfserviceSelectable: true}, {ID: "email", URL: "file://stub/email.schema.json", SelfserviceSelectable: true}, {ID: "phone", URL: "file://stub/phone.schema.json"}, {ID: "not-allowed", URL: "file://stub/login.schema.json"}, }) // We set default to phone and select email in InitializeRegistrationFlowViaBrowser conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "phone") conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, "https://www.ory.com") browserClient := testhelpers.NewClientWithCookies(t) expected := container.Container{ Method: "POST", Nodes: node.Nodes{ node.NewCSRFNode(nosurfx.FakeCSRFToken), node.NewInputField("traits.email", nil, node.DefaultGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { a.Autocomplete = node.InputAttributeAutocompleteEmail })).WithMetaLabel(text.NewInfoNodeLabelGenerated("E-Mail", "traits.email")), node.NewInputField("password", nil, node.PasswordGroup, node.InputAttributeTypePassword, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { a.Autocomplete = node.InputAttributeAutocompleteNewPassword })).WithMetaLabel(text.NewInfoNodeInputPassword()), node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoRegistration()), }, } for _, tc := range []struct { name string isAPI bool isSPA bool }{ { name: "api", isAPI: true, }, { name: "spa", isSPA: true, }, { name: "browser", }, } { t.Run("type="+tc.name+" registration success", func(t *testing.T) { var f *kratos.RegistrationFlow var hc *http.Client var payload string if tc.isAPI { f = testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("email")) hc = apiClient } else { f = testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, tc.isSPA, false, false, testhelpers.InitFlowWithIdentitySchema("email")) hc = browserClient } expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id assertx.EqualAsJSON(t, expected, f.Ui) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) values.Set("traits.email", testhelpers.RandomEmail()) values.Set("password", x.NewUUID().String()) var expectedURL string if tc.isAPI { payload = testhelpers.EncodeFormAsJSON(t, true, values) } else { payload = values.Encode() } if tc.name == "browser" { expectedURL = redirTS.URL } else { expectedURL = conf.SelfPublicURL(t.Context()).String() + login.RouteSubmitFlow } actual, resp := testhelpers.RegistrationMakeRequest(t, tc.isAPI, tc.isSPA, f, hc, payload) require.EqualValues(t, http.StatusOK, resp.StatusCode) if tc.isAPI { assert.NotEmpty(t, gjson.Get(actual, "identity.id").String(), "%s", actual) assert.Equal(t, gjson.Get(actual, "identity.traits.email").String(), values.Get("traits.email"), "%s", actual) assert.Equal(t, gjson.Get(actual, "identity.schema_id").String(), "email", "%s", actual) } identity, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(t.Context(), identity.CredentialsTypePassword, values.Get("traits.email")) require.NoError(t, err, sqlcon.ErrNoRows()) assert.NotEmpty(t, identity.ID) assert.Equalf(t, values.Get("traits.email"), gjson.GetBytes(identity.Traits, "email").String(), "%s", identity.Traits) assert.Equal(t, "email", identity.SchemaID) // login loginValues := func(v url.Values) { v.Set("identifier", values.Get("traits.email")) v.Set("password", values.Get("password")) } body := testhelpers.SubmitLoginForm(t, tc.isAPI, nil, publicTS, loginValues, tc.isSPA, false, http.StatusOK, expectedURL, testhelpers.InitFlowWithIdentitySchema("email")) if tc.name == "browser" { assert.Equalf(t, identity.ID.String(), gjson.Get(body, "identity.id").String(), "%s", body) } else { assert.Equalf(t, identity.ID.String(), gjson.Get(body, "session.identity.id").String(), "%s", body) } }) t.Run("type="+tc.name+" registration fail due to invalid form data", func(t *testing.T) { var f *kratos.RegistrationFlow var hc *http.Client var payload string if tc.isAPI { f = testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("email")) hc = apiClient } else { f = testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, tc.isSPA, false, false, testhelpers.InitFlowWithIdentitySchema("email")) hc = browserClient } expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id assertx.EqualAsJSON(t, expected, f.Ui) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) values.Set("traits.email", "invalidemail") values.Set("password", x.NewUUID().String()) if tc.isAPI { payload = testhelpers.EncodeFormAsJSON(t, true, values) } else { payload = values.Encode() } actual, resp := testhelpers.RegistrationMakeRequest(t, tc.isAPI, tc.isSPA, f, hc, payload) if tc.name == "browser" { require.EqualValues(t, http.StatusOK, resp.StatusCode) } else { require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) } assert.EqualValuesf(t, 4000040, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", actual) assert.Equalf(t, "Enter a valid email address", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", actual) }) } t.Run("type=browser schema=default", func(t *testing.T) { t.Cleanup(func() { conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "phone") }) // We set default to email conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "email") f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id assertx.EqualAsJSON(t, expected, f.Ui) }) t.Run("type=api schema=default", func(t *testing.T) { t.Cleanup(func() { conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "phone") }) // We set default to email conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "email") f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) expected.Action = conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id assertx.EqualAsJSON(t, expected, f.Ui) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) values.Set("traits.email", testhelpers.RandomEmail()) values.Set("password", x.NewUUID().String()) actual, _ := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) assert.NotEmptyf(t, gjson.Get(actual, "identity.id").String(), "%s", actual) assert.Containsf(t, gjson.Get(actual, "identity.traits.email").String(), values.Get("email"), "%s", actual) assert.Containsf(t, gjson.Get(actual, "identity.schema_id").String(), "email", "%s", actual) }) t.Run("type=browser schema=does-not-exist", func(t *testing.T) { testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, true, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, true, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) }) t.Run("type=api schema=does-not-exist", func(t *testing.T) { testhelpers.InitializeRegistrationFlowViaAPIExpectError(t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) testhelpers.InitializeLoginFlowViaAPIExpectError(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithIdentitySchema("does-not-exist")) }) t.Run("type=browser schema=not-allowed", func(t *testing.T) { testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, true, testhelpers.InitFlowWithIdentitySchema("not-allowed")) testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, true, testhelpers.InitFlowWithIdentitySchema("not-allowed")) }) t.Run("type=api schema=not-allowed", func(t *testing.T) { testhelpers.InitializeRegistrationFlowViaAPIExpectError(t, apiClient, publicTS, testhelpers.InitFlowWithIdentitySchema("not-allowed")) testhelpers.InitializeLoginFlowViaAPIExpectError(t.Context(), t, apiClient, publicTS, false, testhelpers.InitFlowWithIdentitySchema("not-allowed")) }) }) }) t.Run("method=PopulateSignUpMethod", func(t *testing.T) { t.Parallel() conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ config.ViperKeyPublicBaseURL: "https://foo/", config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true, config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true, }), configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/sort.schema.json")), ) publicTS, _ := testhelpers.NewKratosServer(t, reg) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) assertx.EqualAsJSON(t, container.Container{ Action: conf.SelfPublicURL(t.Context()).String() + registration.RouteSubmitFlow + "?flow=" + f.Id, Method: "POST", Nodes: node.Nodes{ node.NewCSRFNode(nosurfx.FakeCSRFToken), node.NewInputField("traits.username", nil, node.DefaultGroup, node.InputAttributeTypeText), node.NewInputField("password", nil, node.PasswordGroup, node.InputAttributeTypePassword, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { a.Autocomplete = node.InputAttributeAutocompleteNewPassword })).WithMetaLabel(text.NewInfoNodeInputPassword()), node.NewInputField("traits.bar", nil, node.DefaultGroup, node.InputAttributeTypeText), node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoRegistration()), }, }, f.Ui) }) } func TestPopulateRegistrationMethod(t *testing.T) { t.Parallel() _, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://stub/identity.schema.json")), ) s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypePassword) require.NoError(t, err) fh, ok := s.(registration.FormHydrator) require.True(t, ok) toSnapshot := func(t *testing.T, f node.Nodes) { t.Helper() // The CSRF token has a unique value that messes with the snapshot - ignore it. f.ResetNodes("csrf_token") snapshotx.SnapshotT(t, f, snapshotx.ExceptNestedKeys("nonce", "src")) } newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) { r := httptest.NewRequest("GET", "/self-service/registration/browser", nil) r = r.WithContext(ctx) t.Helper() f, err := registration.NewFlow(reg, r, flow.TypeBrowser) f.UI.Nodes = make(node.Nodes, 0) require.NoError(t, err) return r, f } t.Run("method=PopulateRegistrationMethod", func(t *testing.T) { r, f := newFlow(t.Context(), t) require.NoError(t, fh.PopulateRegistrationMethod(r, f)) toSnapshot(t, f.UI.Nodes) }) t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) { r, f := newFlow(t.Context(), t) require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) toSnapshot(t, f.UI.Nodes) }) t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) { r, f := newFlow(t.Context(), t) require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) toSnapshot(t, f.UI.Nodes) }) t.Run("method=idempotency", func(t *testing.T) { r, f := newFlow(t.Context(), t) var snapshots []node.Nodes t.Run("case=1", func(t *testing.T) { require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) snapshots = append(snapshots, f.UI.Nodes) toSnapshot(t, f.UI.Nodes) }) t.Run("case=2", func(t *testing.T) { require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) snapshots = append(snapshots, f.UI.Nodes) toSnapshot(t, f.UI.Nodes) }) t.Run("case=3", func(t *testing.T) { require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f)) snapshots = append(snapshots, f.UI.Nodes) toSnapshot(t, f.UI.Nodes) }) t.Run("case=4", func(t *testing.T) { require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f)) snapshots = append(snapshots, f.UI.Nodes) toSnapshot(t, f.UI.Nodes) }) t.Run("case=evaluate", func(t *testing.T) { assertx.EqualAsJSON(t, snapshots[0], snapshots[2]) assertx.EqualAsJSON(t, snapshots[1], snapshots[3]) }) }) }