Files
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

685 lines
28 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package password_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/x/configx"
"github.com/ory/client-go"
"github.com/ory/kratos/corpx"
"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/settingshelpers"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/text"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/x/assertx"
"github.com/ory/x/httpx"
"github.com/ory/x/ioutilx"
"github.com/ory/x/randx"
)
func init() {
corpx.RegisterFakes()
}
func newIdentityWithPassword(email string) *identity.Identity {
return &identity.Identity{
ID: x.NewUUID(),
NID: x.NewUUID(),
Credentials: map[identity.CredentialsType]identity.Credentials{
"password": {
Type: "password",
Identifiers: []string{email},
Config: []byte(`{"hashed_password":"foo"}`),
},
},
State: identity.StateActive,
Traits: identity.Traits(`{"email":"` + email + `"}`),
SchemaID: config.DefaultIdentityTraitsSchemaID,
}
}
func newEmptyIdentity() *identity.Identity {
return &identity.Identity{
ID: x.NewUUID(),
NID: x.NewUUID(),
State: identity.StateActive,
Traits: identity.Traits(`{}`),
SchemaID: config.DefaultIdentityTraitsSchemaID,
}
}
func newIdentityWithoutCredentials(email string) *identity.Identity {
return &identity.Identity{
ID: x.NewUUID(),
NID: x.NewUUID(),
State: identity.StateActive,
Traits: identity.Traits(`{"email":"` + email + `"}`),
SchemaID: config.DefaultIdentityTraitsSchemaID,
}
}
func TestSettings(t *testing.T) {
t.Parallel()
conf, reg := pkg.NewFastRegistryWithMocks(t,
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/profile.schema.json")),
configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, true)),
configx.WithValues(testhelpers.MethodEnableConfig(settings.StrategyProfile, true)),
configx.WithValue(config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m"),
)
settingsUI := testhelpers.NewSettingsUIFlowEchoServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
_ = testhelpers.NewLoginUIWith401Response(t, conf)
browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
apiIdentity1 := newIdentityWithPassword("john-api@doe.com")
browserIdentity2 := newEmptyIdentity()
apiIdentity2 := newEmptyIdentity()
publicTS, _ := testhelpers.NewKratosServer(t, reg)
browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, browserIdentity1)
browserUser1.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL))
browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, browserIdentity2)
browserUser2.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL))
apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, apiIdentity1)
apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, apiIdentity2)
t.Run("case=should reject a new password if it is the same as the old one", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}})
t.Cleanup(func() {
conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
})
cfg := client.NewConfiguration()
u, err := url.Parse(publicTS.URL)
require.NoError(t, err)
cfg.Scheme = u.Scheme
cfg.Host = u.Host
cl := client.NewAPIClient(cfg)
api := cl.FrontendAPI
t.Run("type=api", func(t *testing.T) {
// Create a new account.
password := uuid.Must(uuid.NewV4()).String()
var sessionToken string
{
registrationFlow, _, err := api.CreateNativeRegistrationFlow(t.Context()).Execute()
require.NoError(t, err)
require.NotNil(t, registrationFlow)
registrationBody := client.UpdateRegistrationFlowBody{
UpdateRegistrationFlowWithPasswordMethod: &client.UpdateRegistrationFlowWithPasswordMethod{
Method: "password",
Password: password,
Traits: map[string]any{
"email": uuid.Must(uuid.NewV4()).String() + "@ory.dev",
},
},
}
registration, _, err := api.UpdateRegistrationFlow(t.Context()).Flow(registrationFlow.Id).UpdateRegistrationFlowBody(registrationBody).Execute()
require.NoError(t, err)
require.NotNil(t, registration)
require.NotNil(t, registration.SessionToken)
sessionToken = *registration.SessionToken
require.NotEmpty(t, sessionToken)
}
// Create a settings flow.
var settingsFlow *client.SettingsFlow
{
var err error
settingsFlow, _, err = api.CreateNativeSettingsFlow(t.Context()).XSessionToken(sessionToken).Execute()
require.NoError(t, err)
require.NotNil(t, settingsFlow)
}
// Try to set the same password: fails.
{
update := client.UpdateSettingsFlowBody{
UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{
Method: "password",
Password: password,
},
}
req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).XSessionToken(sessionToken)
settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req)
require.Error(t, err)
require.Nil(t, settingsFlow)
require.NotNil(t, httpResp)
require.Equal(t, http.StatusBadRequest, httpResp.StatusCode)
}
// Try to set a different password: succeeds.
{
update := client.UpdateSettingsFlowBody{
UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{
Method: "password",
Password: uuid.Must(uuid.NewV4()).String(),
},
}
req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).XSessionToken(sessionToken)
settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req)
require.NoError(t, err)
require.NotNil(t, settingsFlow)
require.NotNil(t, httpResp)
require.Equal(t, http.StatusOK, httpResp.StatusCode)
}
})
t.Run("type=browser", func(t *testing.T) {
// Create a new account.
password := uuid.Must(uuid.NewV4()).String()
var cookie string
{
registrationFlow, _, err := api.CreateBrowserRegistrationFlow(t.Context()).Execute()
require.NoError(t, err)
require.NotNil(t, registrationFlow)
csrfToken := registrationFlow.Ui.Nodes[0].Attributes.UiNodeInputAttributes.Value.(string)
require.NotEmpty(t, csrfToken)
registrationBody := client.UpdateRegistrationFlowBody{
UpdateRegistrationFlowWithPasswordMethod: &client.UpdateRegistrationFlowWithPasswordMethod{
Method: "password",
Password: password,
Traits: map[string]any{
"email": uuid.Must(uuid.NewV4()).String() + "@ory.dev",
},
CsrfToken: &csrfToken,
},
}
registration, httpResp, err := api.UpdateRegistrationFlow(t.Context()).Flow(registrationFlow.Id).UpdateRegistrationFlowBody(registrationBody).Execute()
require.NoError(t, err)
require.NotNil(t, httpResp)
require.NotNil(t, registration)
cookie = httpResp.Header.Get("Set-Cookie")
require.NotEmpty(t, cookie)
}
// Create a settings flow.
var settingsFlow *client.SettingsFlow
var csrfToken string
{
var err error
settingsFlow, _, err = api.CreateBrowserSettingsFlow(t.Context()).Cookie(cookie).Execute()
require.NoError(t, err)
require.NotNil(t, settingsFlow)
csrfToken = settingsFlow.Ui.Nodes[0].Attributes.UiNodeInputAttributes.Value.(string)
require.NotEmpty(t, csrfToken)
}
// Try to set the same password: fails.
{
update := client.UpdateSettingsFlowBody{
UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{
Method: "password",
Password: password,
CsrfToken: &csrfToken,
},
}
req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).Cookie(cookie)
settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req)
require.Error(t, err)
require.Nil(t, settingsFlow)
require.NotNil(t, httpResp)
require.Equal(t, http.StatusBadRequest, httpResp.StatusCode)
}
// Try to set a different password: succeeds.
{
update := client.UpdateSettingsFlowBody{
UpdateSettingsFlowWithPasswordMethod: &client.UpdateSettingsFlowWithPasswordMethod{
Method: "password",
Password: uuid.Must(uuid.NewV4()).String(),
CsrfToken: &csrfToken,
},
}
req := api.UpdateSettingsFlow(t.Context()).UpdateSettingsFlowBody(update).Flow(settingsFlow.Id).Cookie(cookie)
settingsFlow, httpResp, err := api.UpdateSettingsFlowExecute(req)
require.NoError(t, err)
require.NotNil(t, settingsFlow)
require.NotNil(t, httpResp)
require.Equal(t, http.StatusOK, httpResp.StatusCode)
}
})
})
t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) {
c := testhelpers.NewDebugClient(t)
t.Run("type=browser", func(t *testing.T) {
res, err := c.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/x-www-form-urlencoded"))
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceLoginUI))
})
t.Run("type=spa", func(t *testing.T) {
res, err := c.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/json"))
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request)
assert.Contains(t, res.Request.URL.String(), settings.RouteSubmitFlow)
})
t.Run("type=api", func(t *testing.T) {
res, err := c.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(`{"foo":"bar"}`), "application/json"))
require.NoError(t, err)
assert.Len(t, res.Cookies(), 0)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode)
})
})
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(t.Context()).String()))
}
t.Run("description=should fail if password violates policy", func(t *testing.T) {
check := func(t *testing.T, reason, actual string) {
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).attributes.value").String(), "%s", actual)
assert.NotEmpty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", actual)
assert.Equal(t, reason, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0.text").String(), "%s", actual)
}
t.Run("session=with privileged session", func(t *testing.T) {
conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m")
payload := func(v url.Values) {
v.Set("password", "123456")
v.Set("method", "password")
}
t.Run("type=api", func(t *testing.T) {
check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("spa=spa", func(t *testing.T) {
check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, false, false, browserUser1, payload))
})
})
t.Run("session=needs reauthentication", func(t *testing.T) {
conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns")
defer testhelpers.NewLoginUIWith401Response(t, conf)
t.Cleanup(func() {
conf.MustSet(context.Background(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m")
})
payload := func(v url.Values) {
v.Set("method", "password")
v.Set("password", "123456")
}
t.Run("type=api/expected=an error because reauth can not be initialized for API clients", func(t *testing.T) {
_ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, apiUser1), conf)
actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload,
http.StatusForbidden, publicTS.URL+settings.RouteSubmitFlow)
assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"})
assert.NotEmpty(t, json.RawMessage(gjson.Get(actual, "redirect_browser_to").String()))
})
t.Run("type=spa", func(t *testing.T) {
_ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, browserUser1), conf)
actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload,
http.StatusForbidden, publicTS.URL+settings.RouteSubmitFlow)
assertx.EqualAsJSON(t, settings.NewFlowNeedsReAuth().DefaultError, json.RawMessage(gjson.Get(actual, "error").Raw))
assert.NotEmpty(t, json.RawMessage(gjson.Get(actual, "redirect_browser_to").String()))
})
t.Run("type=browser", func(t *testing.T) {
_ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, browserUser1), conf)
check(t, "The password must be at least 8 characters long, but got 6.", expectValidationError(t, false, false, browserUser1, payload))
})
})
})
t.Run("description=should not be able to make requests for another user", func(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "password")
values.Set("password", x.NewUUID().String())
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values))
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "password")
values.Set("password", x.NewUUID().String())
actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, values.Encode())
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "password")
values.Set("password", x.NewUUID().String())
actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser2, values.Encode())
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "reason").String(), "initiated by someone else", "%s", actual)
})
})
t.Run("description=should update the password and clear errors if everything is ok", func(t *testing.T) {
check := func(t *testing.T, actual string) {
assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).value").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0.text").String(), actual)
}
payload := func(v url.Values) {
v.Set("method", "password")
v.Set("password", x.NewUUID().String())
}
t.Run("type=api", func(t *testing.T) {
actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow)
check(t, actual)
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow)
check(t, actual)
assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), settingsUI.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
actual := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(t.Context()).String())
check(t, actual)
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "password")
values.Set("password", x.NewUUID().String())
values.Set("csrf_token", "invalid_token")
actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser1, values.Encode())
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceErrorUI))
assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), json.RawMessage(actual), "%s", actual)
})
t.Run("case=should pass even without CSRF token/type=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "password")
values.Set("password", x.NewUUID().String())
values.Set("csrf_token", "invalid_token")
actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser1, testhelpers.EncodeFormAsJSON(t, true, values))
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual)
})
t.Run("case=should pass even without CSRF token/type=api", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "password")
values.Set("password", x.NewUUID().String())
values.Set("csrf_token", "invalid_token")
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser1, testhelpers.EncodeFormAsJSON(t, true, values))
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
assert.NotEmpty(t, gjson.Get(actual, "identity.id").String(), "%s", actual)
})
t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
for k, tc := range []struct {
mod func(http.Header)
exp string
}{
{
mod: func(h http.Header) {
h.Add("Cookie", "name=bar")
},
exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
},
{
mod: func(h http.Header) {
h.Add("Origin", "www.bar.com")
},
exp: "The HTTP Request Header included the \\\"Origin\\\" key",
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("password", x.NewUUID().String())
req := testhelpers.NewPostRequest(t, true, f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values)))
tc.mod(req.Header)
res, err := apiUser1.Do(req)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
actual := string(ioutilx.MustReadAll(res.Body))
assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
assert.Contains(t, actual, tc.exp)
})
}
})
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(t.Context()).String()))
}
t.Run("description=should update the password even if no password was set before", func(t *testing.T) {
bi := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
si := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
ai := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, bi)
spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, si)
apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, ai)
check := func(t *testing.T, actual string, id *identity.Identity) {
assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(name==password).attributes.value").String(), "%s", actual)
actualIdentity, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), id.ID)
require.NoError(t, err)
cfg := string(actualIdentity.Credentials[identity.CredentialsTypePassword].Config)
assert.Contains(t, cfg, "hashed_password", "%+v", actualIdentity.Credentials)
require.Len(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers, 1)
assert.Contains(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers[0], "-4")
}
payload := func(v url.Values) {
v.Set("method", "password")
v.Set("password", randx.MustString(16, randx.AlphaNum))
}
t.Run("type=api", func(t *testing.T) {
actual := expectSuccess(t, true, false, apiUser, payload)
check(t, actual, ai)
})
t.Run("type=spa", func(t *testing.T) {
actual := expectSuccess(t, false, true, spaUser, payload)
check(t, actual, si)
})
t.Run("type=browser", func(t *testing.T) {
actual := expectSuccess(t, false, false, browserUser, payload)
check(t, actual, bi)
})
})
t.Run("description=should update the password and perform the correct redirection", func(t *testing.T) {
rts := testhelpers.NewRedirTS(t, "", conf)
conf.MustSet(t.Context(), config.ViperKeySelfServiceSettingsAfter+"."+config.DefaultBrowserReturnURL, rts.URL+"/return-ts")
t.Cleanup(func() {
conf.MustSet(context.Background(), config.ViperKeySelfServiceSettingsAfter, nil)
})
run := func(t *testing.T, f *kratos.SettingsFlow, isAPI bool, c *http.Client, _ *identity.Identity) {
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "password")
values.Set("password", randx.MustString(16, randx.AlphaNum))
_, res := testhelpers.SettingsMakeRequest(t, isAPI, false, f, c, testhelpers.EncodeFormAsJSON(t, isAPI, values))
require.EqualValues(t, rts.URL+"/return-ts", res.Request.URL.String())
actualIdentity, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), browserIdentity1.ID)
require.NoError(t, err)
cfg := string(actualIdentity.Credentials[identity.CredentialsTypePassword].Config)
assert.NotContains(t, cfg, "foo")
assert.NotEqual(t, `{"hashed_password":"foo"}`, cfg)
}
t.Run("type=api", func(t *testing.T) {
t.Skip("Post-registration redirects do not work for API flows and are thus not tested here.")
})
t.Run("type=spa", func(t *testing.T) {
t.Skip("Post-registration redirects do not work for API flows and are thus not tested here.")
})
t.Run("type=browser", func(t *testing.T) {
rs := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
run(t, rs, false, browserUser1, browserIdentity1)
})
})
t.Run("description=should update the password and revoke other user sessions", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, "password"), []config.SelfServiceHook{{Name: "revoke_active_sessions"}})
t.Cleanup(func() {
conf.MustSet(context.Background(), config.ViperKeySelfServiceSettingsAfter, nil)
})
check := func(t *testing.T, actual string, id *identity.Identity) {
assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(name==password).attributes.value").String(), "%s", actual)
actualIdentity, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), id.ID)
require.NoError(t, err)
cfg := string(actualIdentity.Credentials[identity.CredentialsTypePassword].Config)
assert.Contains(t, cfg, "hashed_password", "%+v", actualIdentity.Credentials)
require.Len(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers, 1)
assert.Contains(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers[0], "-4")
}
initClients := func(isAPI bool, id *identity.Identity) (client1, client2 *http.Client) {
if isAPI {
client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id)
client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id)
return client1, client2
}
client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id)
client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id)
return client1, client2
}
run := func(t *testing.T, isAPI, isSPA bool, id *identity.Identity) {
payload := func(v url.Values) {
v.Set("method", "password")
v.Set("password", randx.MustString(16, randx.AlphaNum))
}
user1, user2 := initClients(isAPI, id)
actual := expectSuccess(t, isAPI, isSPA, user1, payload)
check(t, actual, id)
// second client should be logged out
res, err := user2.Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/json"))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request)
// again change password via first client
actual = expectSuccess(t, isAPI, isSPA, user1, payload)
check(t, actual, id)
}
t.Run("type=api", func(t *testing.T) {
id := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
run(t, true, false, id)
})
t.Run("type=spa", func(t *testing.T) {
id := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
run(t, false, true, id)
})
t.Run("type=browser", func(t *testing.T) {
id := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
run(t, false, false, id)
})
})
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")
id := newIdentityWithoutCredentials(testhelpers.RandomEmail())
browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id)
api := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id)
for _, f := range []string{"spa", "api", "browser"} {
t.Run("type="+f, func(t *testing.T) {
hc := browser
if f == "api" {
hc = api
}
actual := settingshelpers.ExpectValidationError(t, publicTS, hc, conf, f, func(v url.Values) {
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)
})
}
})
}