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

405 lines
18 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package registration_test
import (
"context"
"fmt"
"net/http"
nethttptest "net/http/httptest"
"net/url"
"testing"
"github.com/gobuffalo/httptest"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hydra"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/hook"
"github.com/ory/kratos/session"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/x/sqlcon"
"github.com/ory/x/sqlxx"
)
func TestRegistrationExecutor(t *testing.T) {
t.Parallel()
ctx := context.Background()
for _, strategy := range identity.AllCredentialTypes {
strategy := strategy.String()
t.Run("strategy="+strategy, func(t *testing.T) {
t.Parallel()
conf, reg := pkg.NewFastRegistryWithMocks(t)
reg.SetHydra(hydra.NewFake())
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToServer.URL)
newServer := func(t *testing.T, i *identity.Identity, ft flow.Type, flowCallbacks ...func(*registration.Flow)) *httptest.Server {
router := http.NewServeMux()
handleErr := testhelpers.SelfServiceHookRegistrationErrorHandler
router.HandleFunc("GET /registration/pre", func(w http.ResponseWriter, r *http.Request) {
f, err := registration.NewFlow(reg, r, ft)
require.NoError(t, err)
if handleErr(t, w, r, reg.RegistrationHookExecutor().PreRegistrationHook(w, r, f)) {
_, _ = w.Write([]byte("ok"))
}
})
router.HandleFunc("GET /registration/post", func(w http.ResponseWriter, r *http.Request) {
if i == nil {
i = testhelpers.SelfServiceHookFakeIdentity(t)
}
regFlow, err := registration.NewFlow(reg, r, ft)
require.NoError(t, err)
regFlow.RequestURL = x.RequestURL(r).String()
for _, callback := range flowCallbacks {
callback(regFlow)
}
_ = handleErr(t, w, r, reg.RegistrationHookExecutor().PostRegistrationHook(w, r, regFlow, i, session.AuthenticationMethod{
Method: identity.CredentialsType(strategy),
AAL: identity.AuthenticatorAssuranceLevel1,
}))
})
ts := httptest.NewServer(router)
t.Cleanup(ts.Close)
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL)
return ts
}
makeRequestPost := testhelpers.SelfServiceMakeRegistrationPostHookRequest
viperSetPost := testhelpers.SelfServiceHookRegistrationViperSetPost
t.Run("method=PostRegistrationHook", func(t *testing.T) {
t.Run("case=pass without hooks", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
i := testhelpers.SelfServiceHookFakeIdentity(t)
ts := newServer(t, i, flow.TypeBrowser)
res, body := makeRequestPost(t, ts, false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, returnToServer.URL, res.Request.URL.String())
actual, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
require.NoError(t, err)
assert.Equal(t, actual.Traits, i.Traits)
})
t.Run("case=pass without hooks if ajax client", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
i := testhelpers.SelfServiceHookFakeIdentity(t)
ts := newServer(t, i, flow.TypeBrowser)
res, body := makeRequestPost(t, ts, true, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.Contains(t, res.Request.URL.String(), ts.URL)
assert.JSONEq(t, fmt.Sprintf(`[{"action":"redirect_browser_to","redirect_browser_to":"%s"}]`, returnToServer.URL), gjson.Get(body, "continue_with").Raw)
actual, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
require.NoError(t, err)
assert.Equal(t, actual.Traits, i.Traits)
})
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
viperSetPost(t, conf, strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})
res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, returnToServer.URL, res.Request.URL.String())
})
t.Run("case=fail if hooks fail", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
viperSetPost(t, conf, strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecutePostRegistrationPrePersistHook": "abort"}`)}})
i := testhelpers.SelfServiceHookFakeIdentity(t)
res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.Equal(t, "", body)
_, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
require.Error(t, err)
})
t.Run("case=use return_to value", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToServer.URL})
res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{"return_to": {returnToServer.URL + "/kratos"}})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String())
})
t.Run("case=use nested config value", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
testhelpers.SelfServiceHookRegistrationSetDefaultRedirectToStrategy(t, conf, strategy, returnToServer.URL+"/kratos")
res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String())
})
t.Run("case=use nested config value", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToServer.URL + "/kratos"})
testhelpers.SelfServiceHookRegistrationSetDefaultRedirectTo(t, conf, returnToServer.URL+"/not-kratos")
testhelpers.SelfServiceHookRegistrationSetDefaultRedirectToStrategy(t, conf, strategy, returnToServer.URL+"/kratos")
res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, returnToServer.URL+"/kratos", res.Request.URL.String())
})
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
viperSetPost(t, conf, strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})
res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, returnToServer.URL, res.Request.URL.String())
})
t.Run("case=send a json response for API clients", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
res, body := makeRequestPost(t, newServer(t, nil, flow.TypeAPI), true, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.NotEmpty(t, gjson.Get(body, "identity.id"))
})
t.Run("case=pass without hooks for browser flow with application/json", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
res, body := makeRequestPost(t, newServer(t, nil, flow.TypeBrowser), true, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.NotEmpty(t, gjson.Get(body, "identity.id"))
assert.Empty(t, gjson.Get(body, "session.token"))
assert.Empty(t, gjson.Get(body, "session_token"))
})
t.Run("case=should redirect to verification UI if show_verification_ui hook is set", func(t *testing.T) {
verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg)
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{
{
"hook": hook.KeyVerificationUI,
},
})
i := testhelpers.SelfServiceHookFakeIdentity(t)
i.Traits = identity.Traits(`{"email": "verifiable@ory.sh"}`)
res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.Contains(t, res.Request.URL.String(), verificationTS.URL)
assert.NotEmpty(t, res.Request.URL.Query().Get("flow"))
})
t.Run("case=should redirect to verification UI if there is a login_challenge", func(t *testing.T) {
verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg)
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{{
"hook": hook.KeyVerificationUI,
}})
i := testhelpers.SelfServiceHookFakeIdentity(t)
i.Traits = identity.Traits(`{"email": "verifiable-valid-login_challenge@ory.sh"}`)
withOAuthChallenge := func(f *registration.Flow) {
f.OAuth2LoginChallenge = hydra.FakeValidLoginChallenge
}
res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser, withOAuthChallenge), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.Contains(t, res.Request.URL.String(), verificationTS.URL)
flowID := res.Request.URL.Query().Get("flow")
require.NotEmpty(t, flowID)
flow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, uuid.Must(uuid.FromString(flowID)))
require.NoError(t, err)
assert.Equal(t, hydra.FakeValidLoginChallenge, flow.OAuth2LoginChallenge.String())
})
t.Run("case=should redirect to first verification UI if show_verification_ui hook is set and multiple verifiable addresses", func(t *testing.T) {
verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg)
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{
{
"hook": hook.KeyVerificationUI,
},
})
i := testhelpers.SelfServiceHookFakeIdentity(t)
i.SchemaID = testhelpers.UseIdentitySchema(t, conf, "file://./stub/registration-multi-email.schema.json")
i.Traits = identity.Traits(`{"emails": ["one@ory.sh", "two@ory.sh"]}`)
res, body := makeRequestPost(t, newServer(t, i, flow.TypeBrowser), false, url.Values{})
require.EqualValuesf(t, http.StatusOK, res.StatusCode, "%s", body)
assert.Contains(t, res.Request.URL.String(), verificationTS.URL)
assert.NotEmpty(t, res.Request.URL.Query().Get("flow"))
})
t.Run("case=should still sent session if show_verification_ui is set after session hook", func(t *testing.T) {
verificationTS := testhelpers.NewVerificationUIFlowEchoServer(t, reg)
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{
{
"hook": hook.KeyVerificationUI,
},
{
"hook": hook.KeySessionIssuer,
},
})
i := testhelpers.SelfServiceHookFakeIdentity(t)
i.Traits = identity.Traits(`{"email": "verifiable4@ory.sh"}`)
jar := testhelpers.EasyCookieJar(t, nil)
s := newServer(t, i, flow.TypeBrowser)
s.Client().Jar = jar
res, _ := makeRequestPost(t, s, false, url.Values{})
assert.EqualValues(t, http.StatusOK, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), verificationTS.URL)
assert.NotEmpty(t, res.Request.URL.Query().Get("flow"))
u, err := url.Parse(s.URL)
require.NoError(t, err)
cookies := jar.Cookies(u)
require.Len(t, cookies, 1)
assert.Equal(t, "ory_kratos_session", cookies[0].Name)
})
})
t.Run("case=preserve trait values on duplicate credential error", func(t *testing.T) {
t.Parallel()
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
tosSchema := testhelpers.UseIdentitySchema(t, conf, "file://./stub/registration-tos.schema.json")
// Create an existing identity with a known email so the next registration triggers a unique violation.
existing := &identity.Identity{
SchemaID: tosSchema,
Traits: identity.Traits(`{"email":"duplicate@ory.sh"}`),
State: identity.StateActive,
}
existing.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{
Type: identity.CredentialsTypePassword,
Identifiers: []string{"duplicate@ory.sh"},
Config: []byte(`{"hashed_password":"$argon2id$v=19$m=65536,t=1,p=1$abc$def"}`),
})
require.NoError(t, reg.IdentityManager().Create(context.Background(), existing))
// Build a new identity with the same email and tos=true.
duplicate := testhelpers.SelfServiceHookFakeIdentity(t)
duplicate.SchemaID = tosSchema
duplicate.Traits = identity.Traits(`{"email":"duplicate@ory.sh","tos":true}`)
duplicate.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{
Type: identity.CredentialsTypePassword,
Identifiers: []string{"duplicate@ory.sh"},
Config: []byte(`{"hashed_password":"$argon2id$v=19$m=65536,t=1,p=1$abc$def"}`),
})
regFlow, err := registration.NewFlow(reg, nethttptest.NewRequest("GET", returnToServer.URL, nil), flow.TypeBrowser)
require.NoError(t, err)
regFlow.RequestURL = returnToServer.URL
// Add a checkbox node for traits.tos (simulating PopulateRegistrationMethod from the profile strategy).
regFlow.UI.Nodes.Upsert(node.NewInputField("traits.tos", false, node.DefaultGroup, node.InputAttributeTypeCheckbox))
err = reg.RegistrationHookExecutor().PostRegistrationHook(
nethttptest.NewRecorder(), nethttptest.NewRequest("GET", returnToServer.URL, nil),
regFlow, duplicate, session.AuthenticationMethod{
Method: identity.CredentialsTypePassword,
AAL: identity.AuthenticatorAssuranceLevel1,
},
)
require.Error(t, err)
// The TOS checkbox node must retain the submitted value (true) from the identity traits.
tosNode := regFlow.UI.Nodes.Find("traits.tos")
require.NotNil(t, tosNode, "traits.tos node must exist in the flow UI")
assert.Equal(t, true, tosNode.GetValue(), "traits.tos must be true after duplicate credential error")
})
for _, kind := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} {
t.Run("type="+string(kind)+"/method=PreRegistrationHook", testhelpers.TestSelfServicePreHook(
config.ViperKeySelfServiceRegistrationBeforeHooks,
testhelpers.SelfServiceMakeRegistrationPreHookRequest,
func(t *testing.T) *httptest.Server {
return newServer(t, nil, kind)
},
conf,
))
}
})
}
}
func TestPostRegistrationHookAccountLinkingOnly(t *testing.T) {
t.Parallel()
ctx := t.Context()
conf, reg := pkg.NewFastRegistryWithMocks(t)
reg.SetHydra(hydra.NewFake())
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToServer.URL)
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnabled, false)
newIdentityWithEmail := func(email string) *identity.Identity {
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"email":"` + email + `"}`)
i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{
Identifiers: []string{email},
Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$fakefakefakefakefakefa"}`),
})
return i
}
runPostRegistrationHook := func(t *testing.T, i *identity.Identity) error {
r := httptest.NewRequest("GET", "/registration/post", nil)
r = r.WithContext(ctx)
regFlow, err := registration.NewFlow(reg, r, flow.TypeAPI)
require.NoError(t, err)
registration.WithFlowAccountLinking()(regFlow)
return reg.RegistrationHookExecutor().PostRegistrationHook(httptest.NewRecorder(), r, regFlow, i, session.AuthenticationMethod{
Method: identity.CredentialsTypePassword,
AAL: identity.AuthenticatorAssuranceLevel1,
})
}
t.Run("case=rejects a new identity when registration is disabled", func(t *testing.T) {
email := "linking-guard-new@ory.sh"
err := runPostRegistrationHook(t, newIdentityWithEmail(email))
require.ErrorContains(t, err, registration.ErrRegistrationDisabled().Error())
_, _, err = reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, email)
require.ErrorIs(t, err, sqlcon.ErrNoRows())
})
t.Run("case=proceeds into the duplicate conflict when an identity exists", func(t *testing.T) {
email := "linking-guard-existing@ory.sh"
existing := newIdentityWithEmail(email)
require.NoError(t, reg.IdentityManager().Create(ctx, existing))
err := runPostRegistrationHook(t, newIdentityWithEmail(email))
// The guard must let the create attempt happen so that the unique
// violation triggers the account linking conversion.
require.ErrorIs(t, err, sqlcon.ErrUniqueViolation())
})
}