Files
upstream-ory-kratos/selfservice/strategy/webauthn/registration_test.go
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

629 lines
27 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package webauthn_test
import (
"context"
_ "embed"
"net/http"
"net/http/httptest"
"net/url"
"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/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/kratos/pkg/registrationhelpers"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/strategy/webauthn"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/x/assertx"
"github.com/ory/x/contextx"
"github.com/ory/x/snapshotx"
)
var (
flows = []string{"spa", "browser"}
//go:embed fixtures/registration/success/response.json
registrationFixtureSuccessResponse []byte
//go:embed fixtures/registration/success/internal_context.json
registrationFixtureSuccessInternalContext []byte
//go:embed fixtures/registration/failure/internal_context_wrong_user_id.json
registrationFixtureFailureInternalContextWrongUserID []byte
)
func flowToIsSPA(flow string) bool {
return flow == "spa"
}
func newRegistrationRegistry(t *testing.T, cfgOpts ...configx.OptionModifier) *driver.RegistryDefault {
_, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{
configx.WithValues(map[string]any{
config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true,
config.ViperKeyWebAuthnPasswordless: true,
config.ViperKeySelfServiceRegistrationLoginHints: true,
config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true,
}),
enabledWebauthn,
}, cfgOpts...)...)
return reg
}
func TestRegistration(t *testing.T) {
reg := newRegistrationRegistry(t, configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/registration.schema.json")))
conf := reg.Config()
publicTS, _ := testhelpers.NewKratosServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
_ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg)
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
redirTS := testhelpers.NewRedirSessionEchoTS(t, reg)
redirNoSessionTS := testhelpers.NewRedirNoSessionTS(t, reg)
// set the "return to" server, which will assert the session state
// (redirTS: enforce that a session exists, redirNoSessionTS: enforce that no session exists)
useReturnToFromTS := func(ts *httptest.Server) {
conf.MustSet(t.Context(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to")
conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts")
}
useReturnToFromTS(redirTS)
//checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) {
// if shouldRedirect {
// assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts")
// } else {
// assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow)
// }
//}
t.Run("AssertCommonErrorCases", func(t *testing.T) {
registrationhelpers.AssertCommonErrorCases(t, flows)
})
t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) {
reg := newRegistrationRegistry(t)
registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) {
v.Del("traits.foobar")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
})
})
t.Run("AssertCSRFFailures", func(t *testing.T) {
reg := newRegistrationRegistry(t)
registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) {
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
})
})
t.Run("AssertSchemaDoesNotExist", func(t *testing.T) {
reg := newRegistrationRegistry(t)
registrationhelpers.AssertSchemaDoesNotExist(t, reg, flows, func(v url.Values) {
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
})
})
t.Run("case=webauthn button does not exist when passwordless is disabled", func(t *testing.T) {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, false)
t.Cleanup(func() {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, true)
})
for _, f := range flows {
t.Run(f, func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, flowToIsSPA(f), false, false)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
})
})
}
})
t.Run("case=webauthn button exists", func(t *testing.T) {
for _, f := range flows {
t.Run(f, func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, flowToIsSPA(f), false, false)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
"5.attributes.onclick",
"5.attributes.value",
"6.attributes.nonce",
"6.attributes.src",
})
})
}
})
t.Run("case=should return an error because not passing validation", func(t *testing.T) {
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.username", email)
v.Del("traits.foobar")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values)
assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual)
assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual)
})
}
})
t.Run("case=should reject invalid transient payload", func(t *testing.T) {
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bar")
v.Set("transient_payload", "42")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values)
assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Equal(t, "bar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual)
assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual)
assert.Equal(t, int64(4000026), gjson.Get(actual, "ui.nodes.#(attributes.name==transient_payload).messages.0.id").Int(), "%s", actual)
})
}
})
t.Run("case=should return an error because webauthn response is invalid", func(t *testing.T) {
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values)
assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual)
assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.messages.0").String(), `Unable to parse WebAuthn response: Parse error for Registration`, "%s", actual)
})
}
})
submitWebAuthnRegistrationWithClient := func(t *testing.T, flow string, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.RegistrationFlow) {
isSPA := flow == "spa"
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, isSPA, false, false, opts...)
// We inject the session to replay
interim, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
interim.InternalContext = contextFixture
require.NoError(t, reg.RegistrationFlowPersister().UpdateRegistrationFlow(context.Background(), interim))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
cb(values)
// We use the response replay
body, res := testhelpers.RegistrationMakeRequest(t, false, isSPA, f, client, values.Encode())
return body, res, f
}
t.Run("case=should return an error because internal context is invalid", func(t *testing.T) {
email := testhelpers.RandomEmail()
for _, tc := range []struct {
name string
internalContext string
}{{
name: "invalid json",
internalContext: "invalid",
}, {
name: "wrong user ID",
internalContext: string(registrationFixtureFailureInternalContextWrongUserID),
}} {
tc := tc
t.Run("context="+tc.name, func(t *testing.T) {
values := func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual, _, _ := submitWebAuthnRegistrationWithClient(t, f,
[]byte(tc.internalContext),
testhelpers.NewClientWithCookies(t),
values,
)
if f == "spa" {
assert.Equal(t, "Internal Server Error", gjson.Get(actual, "error.status").String(), "%s", actual)
} else {
assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual)
}
})
}
})
}
})
t.Run("case=should fail to create identity if schema is missing the identifier", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/noid.schema.json")
t.Cleanup(func() {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
})
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.email", email)
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual, _, _ := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), values)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.email")
assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
})
}
})
makeRegistration := func(t *testing.T, f string, values func(v url.Values), opts ...testhelpers.InitFlowWithOption) (actual string, res *http.Response, fetchedFlow *registration.Flow) {
actual, res, actualFlow := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), values, opts...)
fetchedFlow, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(actualFlow.Id))
require.NoError(t, err)
return actual, res, fetchedFlow
}
makeSuccessfulRegistration := func(t *testing.T, f string, expectReturnTo string, values func(v url.Values), opts ...testhelpers.InitFlowWithOption) (actual string) {
actual, res, fetchedFlow := makeRegistration(t, f, values, opts...)
assert.Empty(t, gjson.GetBytes(fetchedFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)), "has cleaned up the internal context after success")
if f == "spa" {
expectReturnTo = publicTS.URL
}
assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual))
return actual
}
getPrefix := func(f string) (prefix string) {
if f == "spa" {
prefix = "session."
}
return
}
t.Run("successful registration", func(t *testing.T) {
t.Cleanup(func() {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), nil)
})
values := func(email string) func(v url.Values) {
return func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
}
t.Run("case=should create the identity but not a session", func(t *testing.T) {
useReturnToFromTS(redirNoSessionTS)
t.Cleanup(func() {
useReturnToFromTS(redirTS)
})
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirNoSessionTS.URL+"/registration-return-ts", values(email))
if f == "spa" {
assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual)
assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
} else {
assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
}
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
if f == "spa" {
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(), redirNoSessionTS.URL+"/registration-return-ts", "%s", actual)
} else {
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
}
})
}
})
t.Run("case=should accept valid transient payload", func(t *testing.T) {
useReturnToFromTS(redirNoSessionTS)
t.Cleanup(func() {
useReturnToFromTS(redirTS)
})
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirNoSessionTS.URL+"/registration-return-ts", func(v url.Values) {
values(email)(v)
v.Set("transient_payload.stuff", "42")
})
if f == "spa" {
assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual)
assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
} else {
assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
}
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
})
}
})
t.Run("case=should create the identity and a session and use the correct schema", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "advanced-user")
conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "does-not-exist", URL: "file://./stub/profile.schema.json"},
{ID: "advanced-user", URL: "file://./stub/registration.schema.json"},
})
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email))
prefix := getPrefix(f)
assert.Equal(t, email, gjson.Get(actual, prefix+"identity.traits.username").String(), "%s", actual)
assert.True(t, gjson.Get(actual, prefix+"active").Bool(), "%s", actual)
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
})
}
})
t.Run("case=not able to create the same account twice", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email))
assert.True(t, gjson.Get(actual, getPrefix(f)+"active").Bool(), "%s", actual)
actual, _, _ = makeRegistration(t, f, values(email))
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username")
assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your passkey or a security key.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
})
}
})
t.Run("case=reset previous form errors", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual, _, _ := makeRegistration(t, f, func(v url.Values) {
v.Del("traits.username")
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
})
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages.0").String(), `Property username is missing`, "%s", actual)
actual, _, _ = makeRegistration(t, f, func(v url.Values) {
v.Set("traits.username", email)
v.Del("traits.foobar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
})
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages").Array())
assert.Empty(t, gjson.Get(actual, "ui.nodes.messages").Array())
})
}
})
t.Run("case=multi-schema should create the identity and a session and use the correct schema", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "does-not-exist")
conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "does-not-exist", URL: "file://./stub/profile.schema.json"},
{ID: "advanced-user", URL: "file://./stub/registration.schema.json", SelfserviceSelectable: true},
})
for _, f := range flows {
t.Run("type="+f+" registration success", func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email), testhelpers.InitFlowWithIdentitySchema("advanced-user"))
prefix := getPrefix(f)
assert.Equal(t, email, gjson.Get(actual, prefix+"identity.traits.username").String(), "%s", actual)
assert.True(t, gjson.Get(actual, prefix+"active").Bool(), "%s", actual)
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
})
t.Run("type="+f+" registration failure due to invalid form data", func(t *testing.T) {
invalidValues := func(v url.Values) {
v.Set("traits.username", testhelpers.RandomEmail())
v.Set("traits.foobar", "b")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
actual, res, _ := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), invalidValues, testhelpers.InitFlowWithIdentitySchema("advanced-user"))
if f == "browser" {
assert.Equal(t, http.StatusOK, res.StatusCode, "%s", actual)
} else {
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual)
}
assert.Equal(t, int64(4000003), gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.id").Int(), "%s", actual)
assert.Equal(t, "length must be \u003e= 2, but got 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.text").String(), "%s", actual)
})
}
})
})
t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json")
for _, f := range []string{"spa", "api", "browser"} {
t.Run("type="+f, func(t *testing.T) {
actual, _, _ := makeRegistration(t, f, func(v url.Values) {
v.Set("traits.email", testhelpers.RandomEmail())
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
})
assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
})
}
})
}
func TestPopulateRegistrationMethod(t *testing.T) {
ctx := context.Background()
_, reg := pkg.NewFastRegistryWithMocks(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnRPID, "localhost")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnRPDisplayName, "localhost")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true)
s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeWebAuthn)
require.NoError(t, err)
fh, ok := s.(registration.FormHydrator)
require.True(t, ok)
toSnapshot := func(t *testing.T, f node.Nodes, except ...snapshotx.Opt) {
t.Helper()
// The CSRF token has a unique value that messes with the snapshot - ignore it.
f.ResetNodes("csrf_token")
snapshotx.SnapshotT(t, f, append(except, snapshotx.ExceptNestedKeys("nonce", "src", "onclick"))...)
}
newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) {
r := httptest.NewRequest("GET", "/self-service/registration/browser", nil)
r = r.WithContext(ctx)
t.Helper()
f, err := registration.NewFlow(reg, r, flow.TypeBrowser)
f.UI.Nodes = make(node.Nodes, 0)
require.NoError(t, err)
return r, f
}
t.Run("method=PopulateRegistrationMethod", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethod(r, f))
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
toSnapshot(t, f.UI.Nodes)
})
t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("method=idempotency", func(t *testing.T) {
r, f := newFlow(ctx, t)
var snapshots []node.Nodes
t.Run("case=1", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=2", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("case=3", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=4", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("case=evaluate", func(t *testing.T) {
assertx.EqualAsJSON(t, snapshots[0], snapshots[2])
assertx.EqualAsJSONExcept(t, snapshots[1], snapshots[3], []string{"3.attributes.nonce"})
})
})
}