snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
625
selfservice/strategy/lookup/settings_test.go
Normal file
625
selfservice/strategy/lookup/settings_test.go
Normal file
@@ -0,0 +1,625 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package lookup_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/x/snapshotx"
|
||||
|
||||
"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/testhelpers"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/settings"
|
||||
"github.com/ory/kratos/selfservice/strategy/lookup"
|
||||
"github.com/ory/kratos/text"
|
||||
"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/sqlcon"
|
||||
"github.com/ory/x/sqlxx"
|
||||
)
|
||||
|
||||
func createIdentityWithoutLookup(t *testing.T, reg driver.Registry) *identity.Identity {
|
||||
id, _ := createIdentity(t, reg)
|
||||
delete(id.Credentials, identity.CredentialsTypeLookup)
|
||||
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
|
||||
return id
|
||||
}
|
||||
|
||||
func createIdentity(t *testing.T, reg driver.Registry) (*identity.Identity, []identity.RecoveryCode) {
|
||||
codes := make([]identity.RecoveryCode, 12)
|
||||
for k := range codes {
|
||||
var usedAt sqlxx.NullTime
|
||||
if k%3 == 1 {
|
||||
usedAt = sqlxx.NullTime(time.Unix(int64(1629199958+k), 0))
|
||||
}
|
||||
codes[k] = identity.RecoveryCode{Code: fmt.Sprintf("key-%d", k), UsedAt: usedAt}
|
||||
}
|
||||
identifier := x.NewUUID().String() + "@ory.sh"
|
||||
password := x.NewUUID().String()
|
||||
p, err := reg.Hasher(context.Background()).Generate(context.Background(), []byte(password))
|
||||
require.NoError(t, err)
|
||||
i := &identity.Identity{
|
||||
Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
|
||||
VerifiableAddresses: []identity.VerifiableAddress{
|
||||
{
|
||||
Value: identifier,
|
||||
Verified: false,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rc, err := json.Marshal(&identity.CredentialsLookupConfig{RecoveryCodes: codes})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
|
||||
i.Credentials = map[identity.CredentialsType]identity.Credentials{
|
||||
identity.CredentialsTypePassword: {
|
||||
Type: identity.CredentialsTypePassword,
|
||||
Identifiers: []string{identifier},
|
||||
Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`),
|
||||
},
|
||||
identity.CredentialsTypeLookup: {
|
||||
Type: identity.CredentialsTypeLookup,
|
||||
Identifiers: []string{i.ID.String()},
|
||||
Config: rc,
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), i))
|
||||
return i, codes
|
||||
}
|
||||
|
||||
// createIdentityWithImportedLookupViaCreate mimics the admin identity import
|
||||
// path (`POST /admin/identities` with `credentials.lookup_secret`): it
|
||||
// builds an Identity with both password and lookup-secret credentials
|
||||
// populated upfront, leaves `Identifiers` empty on the lookup-secret
|
||||
// credential exactly like `Handler.importLookupSecretCredentials` does,
|
||||
// and persists it via `Manager.Create`. The persister is expected to
|
||||
// default the lookup-secret identifier to the persisted identity ID. See
|
||||
// https://github.com/ory/kratos/issues/4561.
|
||||
func createIdentityWithImportedLookupViaCreate(ctx context.Context, t *testing.T, reg driver.Registry) (*identity.Identity, []identity.RecoveryCode) {
|
||||
codes := make([]identity.RecoveryCode, 4)
|
||||
for k := range codes {
|
||||
codes[k] = identity.RecoveryCode{Code: fmt.Sprintf("imported-key-%d", k)}
|
||||
}
|
||||
identifier := x.NewUUID().String() + "@ory.sh"
|
||||
password := x.NewUUID().String()
|
||||
p, err := reg.Hasher(ctx).Generate(ctx, []byte(password))
|
||||
require.NoError(t, err)
|
||||
rc, err := json.Marshal(&identity.CredentialsLookupConfig{RecoveryCodes: codes})
|
||||
require.NoError(t, err)
|
||||
|
||||
i := &identity.Identity{
|
||||
Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
|
||||
VerifiableAddresses: []identity.VerifiableAddress{
|
||||
{Value: identifier, Verified: false, CreatedAt: time.Now()},
|
||||
},
|
||||
Credentials: map[identity.CredentialsType]identity.Credentials{
|
||||
identity.CredentialsTypePassword: {
|
||||
Type: identity.CredentialsTypePassword,
|
||||
Identifiers: []string{identifier},
|
||||
Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`),
|
||||
},
|
||||
identity.CredentialsTypeLookup: {
|
||||
// Identifiers intentionally empty; the import path does not
|
||||
// know the persisted identity ID yet.
|
||||
Type: identity.CredentialsTypeLookup,
|
||||
Config: rc,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager()))
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, i))
|
||||
return i, codes
|
||||
}
|
||||
|
||||
// createIdentityWithImportedLookupViaUpdate mimics the admin identity
|
||||
// import path on an existing identity (`PUT /admin/identities/{id}` with
|
||||
// `credentials.lookup_secret`): it first creates an identity with a
|
||||
// password credential, then attaches a lookup-secret credential via
|
||||
// `Manager.Update` with `Identifiers` left empty. The persister is
|
||||
// expected to default the lookup-secret identifier to the existing
|
||||
// identity ID. See https://github.com/ory/kratos/issues/4561.
|
||||
func createIdentityWithImportedLookupViaUpdate(ctx context.Context, t *testing.T, reg driver.Registry) (*identity.Identity, []identity.RecoveryCode) {
|
||||
identifier := x.NewUUID().String() + "@ory.sh"
|
||||
password := x.NewUUID().String()
|
||||
p, err := reg.Hasher(ctx).Generate(ctx, []byte(password))
|
||||
require.NoError(t, err)
|
||||
|
||||
i := &identity.Identity{
|
||||
Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
|
||||
VerifiableAddresses: []identity.VerifiableAddress{
|
||||
{Value: identifier, Verified: false, CreatedAt: time.Now()},
|
||||
},
|
||||
Credentials: map[identity.CredentialsType]identity.Credentials{
|
||||
identity.CredentialsTypePassword: {
|
||||
Type: identity.CredentialsTypePassword,
|
||||
Identifiers: []string{identifier},
|
||||
Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager()))
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, i))
|
||||
|
||||
codes := make([]identity.RecoveryCode, 4)
|
||||
for k := range codes {
|
||||
codes[k] = identity.RecoveryCode{Code: fmt.Sprintf("imported-key-%d", k)}
|
||||
}
|
||||
rc, err := json.Marshal(&identity.CredentialsLookupConfig{RecoveryCodes: codes})
|
||||
require.NoError(t, err)
|
||||
i.Credentials[identity.CredentialsTypeLookup] = identity.Credentials{
|
||||
// Identifiers intentionally empty; the persister must default it.
|
||||
Type: identity.CredentialsTypeLookup,
|
||||
Config: rc,
|
||||
}
|
||||
require.NoError(t, i.SetAvailableAAL(ctx, reg.IdentityManager()))
|
||||
require.NoError(t, reg.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits))
|
||||
return i, codes
|
||||
}
|
||||
|
||||
func TestCompleteSettings(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conf, reg := pkg.NewFastRegistryWithMocks(t,
|
||||
configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypePassword, false)),
|
||||
configx.WithValues(testhelpers.MethodEnableConfig("profile", false)),
|
||||
configx.WithValues(testhelpers.MethodEnableConfig(identity.CredentialsTypeLookup, true)),
|
||||
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")),
|
||||
configx.WithValues(map[string]any{
|
||||
config.ViperKeySelfServiceSettingsRequiredAAL: "aal1",
|
||||
config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: "1m",
|
||||
}),
|
||||
)
|
||||
|
||||
publicTS, _ := testhelpers.NewKratosServer(t, reg)
|
||||
|
||||
errTS := testhelpers.NewErrorTestServer(t, reg)
|
||||
uiTS := testhelpers.NewSettingsUIFlowEchoServer(t, reg)
|
||||
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
|
||||
loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg)
|
||||
|
||||
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
|
||||
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
|
||||
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
v(values)
|
||||
payload := testhelpers.EncodeFormAsJSON(t, true, values)
|
||||
return testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, payload)
|
||||
}
|
||||
|
||||
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
|
||||
browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
|
||||
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
v(values)
|
||||
return testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values))
|
||||
}
|
||||
|
||||
t.Run("case=hide recovery codes behind reveal button and show disable button", func(t *testing.T) {
|
||||
id, _ := createIdentity(t, reg)
|
||||
browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id)
|
||||
|
||||
t.Run("case=spa", func(t *testing.T) {
|
||||
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS)
|
||||
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value"))
|
||||
})
|
||||
|
||||
t.Run("case=browser", func(t *testing.T) {
|
||||
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, false, publicTS)
|
||||
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value"))
|
||||
})
|
||||
|
||||
t.Run("case=api", func(t *testing.T) {
|
||||
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
|
||||
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths("0.attributes.value"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("case=button for regeneration is displayed when identity has no recovery codes yet", func(t *testing.T) {
|
||||
id := createIdentityWithoutLookup(t, reg)
|
||||
browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id)
|
||||
|
||||
t.Run("case=spa", func(t *testing.T) {
|
||||
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS)
|
||||
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
|
||||
})
|
||||
|
||||
t.Run("case=browser", func(t *testing.T) {
|
||||
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, false, publicTS)
|
||||
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
|
||||
})
|
||||
|
||||
t.Run("case=api", func(t *testing.T) {
|
||||
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
|
||||
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("case=should pass without csrf if API flow", func(t *testing.T) {
|
||||
id, _ := createIdentity(t, reg)
|
||||
|
||||
body, res := doAPIFlow(t, func(v url.Values) {
|
||||
v.Del("csrf_token")
|
||||
v.Set(node.LookupReveal, "true")
|
||||
}, id)
|
||||
|
||||
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
|
||||
testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes").Raw), []string{"0.attributes.value"})
|
||||
})
|
||||
|
||||
t.Run("case=should fail if CSRF token is invalid", func(t *testing.T) {
|
||||
id := createIdentityWithoutLookup(t, reg)
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
body, res := doBrowserFlow(t, false, func(v url.Values) {
|
||||
v.Del("csrf_token")
|
||||
v.Set(node.LookupReveal, "true")
|
||||
}, id)
|
||||
|
||||
assert.Contains(t, res.Request.URL.String(), errTS.URL)
|
||||
assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "reason").String(), body)
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
body, res := doBrowserFlow(t, true, func(v url.Values) {
|
||||
v.Del("csrf_token")
|
||||
v.Set(node.LookupReveal, "true")
|
||||
}, id)
|
||||
|
||||
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
|
||||
assert.Equal(t, nosurfx.ErrInvalidCSRFToken().Reason(), gjson.Get(body, "error.reason").String(), body)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("type=can not reveal or regenerate or remove without privileged session", func(t *testing.T) {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns")
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m")
|
||||
})
|
||||
|
||||
id, codes := createIdentity(t, reg)
|
||||
|
||||
checkIdentity := func(t *testing.T) {
|
||||
_, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String())
|
||||
require.NoError(t, err)
|
||||
assertx.EqualAsJSON(t, codes, json.RawMessage(gjson.GetBytes(cred.Config, "recovery_codes").Raw))
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
v func(v url.Values)
|
||||
d string
|
||||
}{
|
||||
{
|
||||
d: "reveal",
|
||||
v: func(v url.Values) {
|
||||
v.Set(node.LookupReveal, "true")
|
||||
},
|
||||
},
|
||||
{
|
||||
d: "regenerate",
|
||||
v: func(v url.Values) {
|
||||
v.Set(node.LookupRegenerate, "true")
|
||||
},
|
||||
},
|
||||
{
|
||||
d: "disable",
|
||||
v: func(v url.Values) {
|
||||
v.Set(node.LookupDisable, "true")
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run("case="+tc.d, func(t *testing.T) {
|
||||
payload := tc.v
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
actual, res := doAPIFlow(t, payload, id)
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=")
|
||||
assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"})
|
||||
checkIdentity(t)
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
actual, res := doBrowserFlow(t, true, payload, id)
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
assert.Contains(t, gjson.Get(actual, "redirect_browser_to").String(), publicTS.URL+"/self-service/login/browser?refresh=true&return_to=")
|
||||
assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(actual), []string{"redirect_browser_to"})
|
||||
checkIdentity(t)
|
||||
})
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
actual, res := doBrowserFlow(t, false, payload, id)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, res.Request.URL.String(), loginTS.URL+"/login-ts")
|
||||
assertx.EqualAsJSON(t, text.NewInfoLoginReAuth().Text, json.RawMessage(gjson.Get(actual, "ui.messages.0.text").Raw), actual)
|
||||
checkIdentity(t)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("type=can not confirm without regenerate", func(t *testing.T) {
|
||||
id, codes := createIdentity(t, reg)
|
||||
payload := func(v url.Values) {
|
||||
v.Set(node.LookupConfirm, "true")
|
||||
}
|
||||
|
||||
const reason = "You must (re-)generate recovery backup codes before you can save them."
|
||||
|
||||
checkIdentity := func(t *testing.T) {
|
||||
_, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String())
|
||||
require.NoError(t, err)
|
||||
assertx.EqualAsJSON(t, codes, json.RawMessage(gjson.GetBytes(cred.Config, "recovery_codes").Raw))
|
||||
}
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
actual, res := doAPIFlow(t, payload, id)
|
||||
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
assert.EqualValues(t, reason, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
|
||||
checkIdentity(t)
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
actual, res := doBrowserFlow(t, true, payload, id)
|
||||
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
assert.EqualValues(t, reason, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
|
||||
checkIdentity(t)
|
||||
})
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
actual, res := doBrowserFlow(t, false, payload, id)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
|
||||
assert.EqualValues(t, reason, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
|
||||
checkIdentity(t)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("type=regenerate but no confirmation", func(t *testing.T) {
|
||||
id, codes := createIdentity(t, reg)
|
||||
payload := func(v url.Values) {
|
||||
v.Set(node.LookupRegenerate, "true")
|
||||
}
|
||||
|
||||
checkIdentity := func(t *testing.T) {
|
||||
_, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String())
|
||||
require.NoError(t, err)
|
||||
assertx.EqualAsJSON(t, codes, json.RawMessage(gjson.GetBytes(cred.Config, "recovery_codes").Raw))
|
||||
}
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
actual, res := doAPIFlow(t, payload, id)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(actual, "ui.nodes").Raw), []string{"0.attributes.value"})
|
||||
checkIdentity(t)
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
actual, res := doBrowserFlow(t, true, payload, id)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(actual, "ui.nodes").Raw), []string{"0.attributes.value"})
|
||||
checkIdentity(t)
|
||||
})
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
actual, res := doBrowserFlow(t, false, payload, id)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
|
||||
testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(actual, "ui.nodes").Raw), []string{"0.attributes.value"})
|
||||
checkIdentity(t)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("type=regenerate with confirmation", func(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
d string
|
||||
c func(t *testing.T) *identity.Identity
|
||||
}{
|
||||
{
|
||||
d: "with",
|
||||
c: func(t *testing.T) *identity.Identity {
|
||||
i, _ := createIdentity(t, reg)
|
||||
return i
|
||||
},
|
||||
},
|
||||
{
|
||||
d: "without",
|
||||
c: func(t *testing.T) *identity.Identity {
|
||||
return createIdentityWithoutLookup(t, reg)
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run("credentials="+tc.d, func(t *testing.T) {
|
||||
payload := func(v url.Values) {
|
||||
v.Del(node.LookupReveal)
|
||||
v.Del(node.LookupDisable)
|
||||
v.Set(node.LookupRegenerate, "true")
|
||||
}
|
||||
|
||||
payloadConfirm := func(v url.Values) {
|
||||
v.Del(node.LookupRegenerate)
|
||||
v.Del(node.LookupDisable)
|
||||
v.Del(node.LookupReveal)
|
||||
v.Set(node.LookupConfirm, "true")
|
||||
}
|
||||
|
||||
checkIdentity := func(t *testing.T, id *identity.Identity, f *kratos.SettingsFlow) {
|
||||
_, cred, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String())
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, gjson.GetBytes(cred.Config, "recovery_codes").Raw, "key-1")
|
||||
assert.NotContains(t, gjson.GetBytes(cred.Config, "recovery_codes").Raw, "key-0")
|
||||
|
||||
actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeLookup, lookup.InternalContextKeyRegenerated)))
|
||||
}
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
id, _ := createIdentity(t, reg)
|
||||
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
|
||||
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
|
||||
payload(values)
|
||||
_, _ = testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
|
||||
|
||||
payloadConfirm(values)
|
||||
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
|
||||
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
|
||||
|
||||
checkIdentity(t, id, f)
|
||||
testhelpers.EnsureAAL(t, apiClient, publicTS, "aal2", string(identity.CredentialsTypeLookup))
|
||||
})
|
||||
|
||||
runBrowser := func(t *testing.T, spa bool) {
|
||||
id, _ := createIdentity(t, reg)
|
||||
|
||||
browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
|
||||
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
|
||||
payload(values)
|
||||
_, _ = testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values))
|
||||
|
||||
payloadConfirm(values)
|
||||
actual, res := testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values))
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
if spa {
|
||||
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
|
||||
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(), uiTS.URL, "%s", actual)
|
||||
} else {
|
||||
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
|
||||
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
|
||||
}
|
||||
|
||||
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
|
||||
checkIdentity(t, id, f)
|
||||
testhelpers.EnsureAAL(t, browserClient, publicTS, "aal2", string(identity.CredentialsTypeLookup))
|
||||
}
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
runBrowser(t, false)
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
runBrowser(t, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("type=remove lookup codes", func(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
d string
|
||||
c func(t *testing.T) *identity.Identity
|
||||
}{
|
||||
{
|
||||
d: "with",
|
||||
c: func(t *testing.T) *identity.Identity {
|
||||
i, _ := createIdentity(t, reg)
|
||||
return i
|
||||
},
|
||||
},
|
||||
{
|
||||
d: "without",
|
||||
c: func(t *testing.T) *identity.Identity {
|
||||
return createIdentityWithoutLookup(t, reg)
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run("credentials="+tc.d, func(t *testing.T) {
|
||||
payloadConfirm := func(v url.Values) {
|
||||
v.Del(node.LookupRegenerate)
|
||||
v.Del(node.LookupReveal)
|
||||
v.Set(node.LookupDisable, "true")
|
||||
}
|
||||
|
||||
checkIdentity := func(t *testing.T, id *identity.Identity, f *kratos.SettingsFlow) {
|
||||
_, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeLookup, id.ID.String())
|
||||
require.ErrorIs(t, err, sqlcon.ErrNoRows())
|
||||
|
||||
actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeLookup, lookup.InternalContextKeyRegenerated)).Raw)
|
||||
}
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
id, _ := createIdentity(t, reg)
|
||||
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
|
||||
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
|
||||
payloadConfirm(values)
|
||||
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
|
||||
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
|
||||
|
||||
checkIdentity(t, id, f)
|
||||
testhelpers.EnsureAAL(t, apiClient, publicTS, "aal1")
|
||||
})
|
||||
|
||||
runBrowser := func(t *testing.T, spa bool) {
|
||||
id, _ := createIdentity(t, reg)
|
||||
|
||||
browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, id)
|
||||
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
|
||||
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
|
||||
payloadConfirm(values)
|
||||
actual, res := testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values))
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
if spa {
|
||||
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
|
||||
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(), uiTS.URL, "%s", actual)
|
||||
} else {
|
||||
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
|
||||
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
|
||||
}
|
||||
|
||||
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
|
||||
checkIdentity(t, id, f)
|
||||
testhelpers.EnsureAAL(t, browserClient, publicTS, "aal1")
|
||||
}
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
runBrowser(t, false)
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
runBrowser(t, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user