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
685 lines
28 KiB
Go
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)
|
|
})
|
|
}
|
|
})
|
|
}
|