Files
upstream-ory-kratos/selfservice/strategy/passkey/testfixture_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

416 lines
16 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package passkey_test
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/sjson"
"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/testhelpers"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/x/assertx"
"github.com/ory/x/sqlxx"
)
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.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true,
config.ViperKeySelfServiceRegistrationLoginHints: true,
}),
withEnabledPasskeyStrategy,
}, cfgOpts...)...)
return reg
}
func newLoginRegistry(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.ViperKeySelfServiceRegistrationLoginHints: true,
}),
withEnabledPasskeyStrategy,
}, cfgOpts...)...)
return reg
}
var withEnabledPasskeyStrategy = func() configx.OptionModifier {
key := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePasskey)
return configx.WithValues(map[string]any{
key + ".enabled": true,
key + ".config.rp.display_name": "Ory Corp",
key + ".config.rp.id": "localhost",
key + ".config.rp.origins": []string{"http://localhost:4455"},
})
}()
type fixture struct {
conf *config.Config
reg *driver.RegistryDefault
publicTS *httptest.Server
redirTS *httptest.Server
redirNoSessionTS *httptest.Server
uiTS *httptest.Server
errTS *httptest.Server
loginTS *httptest.Server
}
func newRegistrationFixture(t *testing.T, cfgOpts ...configx.OptionModifier) *fixture {
fix := new(fixture)
fix.reg = newRegistrationRegistry(t, append([]configx.OptionModifier{
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/registration.schema.json")),
configx.WithValue(config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}),
}, cfgOpts...)...)
fix.conf = fix.reg.Config()
fix.publicTS, _ = testhelpers.NewKratosServer(t, fix.reg)
_ = testhelpers.NewErrorTestServer(t, fix.reg)
_ = testhelpers.NewRegistrationUIFlowEchoServer(t, fix.reg)
_ = testhelpers.NewRedirSessionEchoTS(t, fix.reg)
fix.redirTS = testhelpers.NewRedirSessionEchoTS(t, fix.reg)
fix.redirNoSessionTS = testhelpers.NewRedirNoSessionTS(t, fix.reg)
fix.useReturnToFromTS(fix.redirTS)
return fix
}
func newLoginFixture(t *testing.T, cfgOpts ...configx.OptionModifier) *fixture {
fix := new(fixture)
fix.reg = newLoginRegistry(t, append([]configx.OptionModifier{
configx.WithValues(map[string]any{
config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": false,
config.ViperKeySecretsDefault: []string{"not-a-secure-session-key"},
}),
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")),
}, cfgOpts...)...)
fix.conf = fix.reg.Config()
fix.publicTS, _ = testhelpers.NewKratosServer(t, fix.reg)
fix.errTS = testhelpers.NewErrorTestServer(t, fix.reg)
fix.uiTS = testhelpers.NewLoginUIFlowEchoServer(t, fix.reg)
fix.loginTS = fix.uiTS
// Overwrite these two to make it more explicit when tests fail
fix.conf.MustSet(t.Context(), config.ViperKeySelfServiceLoginUI, fix.uiTS.URL+"/login-ts")
fix.redirTS = testhelpers.NewRedirSessionEchoTS(t, fix.reg)
fix.redirNoSessionTS = testhelpers.NewRedirNoSessionTS(t, fix.reg)
fix.useReturnToFromTS(fix.redirTS)
return fix
}
func newSettingsFixture(t *testing.T, cfgOpts ...configx.OptionModifier) *fixture {
fix := newLoginFixture(t, append([]configx.OptionModifier{
configx.WithValues(map[string]any{
config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter: "1m",
config.ViperKeySelfServiceSettingsRequiredAAL: "aal1",
config.ViperKeySessionWhoAmIAAL: "aal1",
config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false,
}),
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/settings.schema.json")),
}, cfgOpts...)...)
fix.uiTS = testhelpers.NewSettingsUIFlowEchoServer(t, fix.reg)
return fix
}
func (fix *fixture) checkURL(t *testing.T, shouldRedirect bool, res *http.Response) {
if shouldRedirect {
assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL+"/login-ts")
} else {
assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow)
}
}
func (fix *fixture) loginViaBrowser(t *testing.T, spa bool, cb func(url.Values), browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) {
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, fix.publicTS, false, spa, false, false, opts...)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
cb(values)
return testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode())
}
func (fix *fixture) createIdentityWithPasskey(t *testing.T, c identity.Credentials) *identity.Identity {
var id identity.Identity
require.NoError(t, json.Unmarshal(loginSuccessIdentity, &id))
id.SetCredentials(identity.CredentialsTypePasskey, identity.Credentials{
Identifiers: []string{"some-random-user-handle"},
Config: c.Config,
Type: identity.CredentialsTypePasskey,
Version: c.Version,
})
// We clean up the identity in case it has been created before
_ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(t.Context(), id.ID)
require.NoError(t, fix.reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), &id))
return &id
}
func (fix *fixture) submitWebAuthnLoginFlowWithClient(t *testing.T, isSPA bool, f *kratos.LoginFlow, contextFixture []byte, client *http.Client, cb func(values url.Values)) (string, *http.Response, *kratos.LoginFlow) {
// We inject the session to replay
interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
interim.InternalContext = contextFixture
require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
cb(values)
// We use the response replay
body, res := testhelpers.LoginMakeRequest(t, false, isSPA, f, client, values.Encode())
return body, res, f
}
func (fix *fixture) submitWebAuthnLoginWithClient(t *testing.T, isSPA bool, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
f := testhelpers.InitializeLoginFlowViaBrowser(t, client, fix.publicTS, false, isSPA, false, false, opts...)
return fix.submitWebAuthnLoginFlowWithClient(t, isSPA, f, contextFixture, client, cb)
}
func (fix *fixture) submitWebAuthnLogin(t *testing.T, ctx context.Context, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, fix.reg, id)
return fix.submitWebAuthnLoginWithClient(t, isSPA, contextFixture, browserClient, cb, opts...)
}
// useReturnToFromTS sets the "return to" server, which will assert the session
// state (redirTS: enforce that a session exists, redirNoSessionTS: enforce that
// no session exists)
func (fix *fixture) useReturnToFromTS(ts *httptest.Server) {
fix.conf.MustSet(context.TODO(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to")
fix.conf.MustSet(context.TODO(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts")
}
func (fix *fixture) useRedirTS() { fix.useReturnToFromTS(fix.redirTS) }
func (fix *fixture) useRedirNoSessionTS() { fix.useReturnToFromTS(fix.redirNoSessionTS) }
func (fix *fixture) disableSessionAfterRegistration() {
fix.conf.MustSet(context.TODO(), config.HookStrategyKey(
config.ViperKeySelfServiceRegistrationAfter,
identity.CredentialsTypePasskey.String(),
), nil)
}
func (fix *fixture) enableSessionAfterRegistration() {
fix.conf.MustSet(context.TODO(), config.HookStrategyKey(
config.ViperKeySelfServiceRegistrationAfter,
identity.CredentialsTypePasskey.String(),
), []config.SelfServiceHook{{Name: "session"}})
}
type submitPasskeyOpt struct {
initFlowOpts []testhelpers.InitFlowWithOption
userID string
internalContext sqlxx.JSONRawMessage
}
type submitPasskeyOption func(o *submitPasskeyOpt)
func withUserID(id string) submitPasskeyOption {
return func(o *submitPasskeyOpt) {
o.userID = base64.StdEncoding.EncodeToString([]byte(id))
}
}
func withInternalContext(ic sqlxx.JSONRawMessage) submitPasskeyOption {
return func(o *submitPasskeyOpt) {
o.internalContext = ic
}
}
func withInitFlowWithOption(ifo []testhelpers.InitFlowWithOption) submitPasskeyOption {
return func(o *submitPasskeyOpt) {
o.initFlowOpts = ifo
}
}
func (fix *fixture) submitPasskeyBrowserRegistration(
t *testing.T,
flowType string,
client *http.Client,
cb func(values url.Values),
opts ...submitPasskeyOption,
) (string, *http.Response, *kratos.RegistrationFlow) {
return fix.submitPasskeyRegistration(t.Context(), t, flowType, client, cb, append([]submitPasskeyOption{withInternalContext(registrationFixtureSuccessBrowserInternalContext)}, opts...)...)
}
func (fix *fixture) submitPasskeyAndroidRegistration(
t *testing.T,
flowType string,
client *http.Client,
cb func(values url.Values),
opts ...submitPasskeyOption,
) (string, *http.Response, *kratos.RegistrationFlow) {
return fix.submitPasskeyRegistration(t.Context(), t, flowType, client, cb,
append([]submitPasskeyOption{withInternalContext(
registrationFixtureSuccessAndroidInternalContext,
)}, opts...)...)
}
func (fix *fixture) submitPasskeyRegistration(
ctx context.Context,
t *testing.T,
flowType string,
client *http.Client,
cb func(values url.Values),
opts ...submitPasskeyOption,
) (string, *http.Response, *kratos.RegistrationFlow) {
o := &submitPasskeyOpt{}
for _, fn := range opts {
fn(o)
}
isSPA := flowType == "spa"
isAPI := flowType == "api"
var regFlow *kratos.RegistrationFlow
if isAPI {
regFlow = testhelpers.InitializeRegistrationFlowViaAPI(t, client, fix.publicTS, o.initFlowOpts...)
} else {
regFlow = testhelpers.InitializeRegistrationFlowViaBrowserCtx(ctx, t, client, fix.publicTS, isSPA, false, false, o.initFlowOpts...)
}
// First step: fill out traits and click on "sign up with passkey"
values := testhelpers.SDKFormFieldsToURLValues(regFlow.Ui.Nodes)
cb(values)
passkeyRegisterVal := values.Get(node.PasskeyRegister) // needed in the second step
values.Del(node.PasskeyRegister)
values.Set("method", "passkey")
// Encode payload based on flow type
var payload string
if isAPI {
payload = testhelpers.EncodeFormAsJSON(t, true, values)
} else {
payload = values.Encode()
}
_, _ = testhelpers.RegistrationMakeRequest(t, isAPI, isSPA, regFlow, client, payload)
// We inject the session to replay
interim, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(ctx, uuid.FromStringOrNil(regFlow.Id))
require.NoError(t, err)
if len(o.internalContext) > 0 {
interim.InternalContext = o.internalContext
}
if o.userID != "" {
interim.InternalContext, err = sjson.SetBytes(interim.InternalContext, "passkey_session_data.user_id", o.userID)
require.NoError(t, err)
}
require.NoError(t, fix.reg.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, interim))
// Second step: fill out passkey response
values.Set(node.PasskeyRegister, passkeyRegisterVal)
if isAPI {
payload = testhelpers.EncodeFormAsJSON(t, true, values)
} else {
payload = values.Encode()
}
body, res := testhelpers.RegistrationMakeRequest(t, isAPI, isSPA, regFlow, client, payload)
return body, res, regFlow
}
func (fix *fixture) makeRegistration(t *testing.T, flowType string, values func(v url.Values), opts ...submitPasskeyOption) (actual string, res *http.Response, fetchedFlow *registration.Flow) {
actual, res, actualFlow := fix.submitPasskeyBrowserRegistration(t, flowType, testhelpers.NewClientWithCookies(t), values, opts...)
fetchedFlow, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(t.Context(), uuid.FromStringOrNil(actualFlow.Id))
require.NoError(t, err)
return actual, res, fetchedFlow
}
func (fix *fixture) makeSuccessfulRegistration(t *testing.T, flowType string, expectReturnTo string, values func(v url.Values), opts ...submitPasskeyOption) (actual string) {
actual, res, _ := fix.makeRegistration(t, flowType, values, opts...)
if flowType == "spa" {
expectReturnTo = fix.publicTS.URL
}
if flowType == "api" {
// API does not redirect; response body should contain JSON with identity/session fields.
// Leave detailed assertions to the calling test.
return actual
}
assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual))
return actual
}
func (fix *fixture) makeUnsuccessfulRegistration(t *testing.T, flowType string, expectReturnTo string, values func(v url.Values), opts ...submitPasskeyOption) (actual string, res *http.Response) {
actual, res, _ = fix.makeRegistration(t, flowType, values, opts...)
return actual, res
}
func (fix *fixture) createIdentityWithoutPasskey(t *testing.T) *identity.Identity {
id := fix.createIdentity(t)
delete(id.Credentials, identity.CredentialsTypePasskey)
require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), id))
return id
}
func (fix *fixture) createIdentityAndReturnIdentifier(t *testing.T, conf []byte) (*identity.Identity, string) {
identifier := x.NewUUID().String() + "@ory.sh"
password := x.NewUUID().String()
p, err := fix.reg.Hasher(t.Context()).Generate(t.Context(), []byte(password))
require.NoError(t, err)
i := &identity.Identity{
Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, identifier)),
VerifiableAddresses: []identity.VerifiableAddress{
{
Value: identifier,
Verified: false,
CreatedAt: time.Now(),
},
},
}
if conf == nil {
conf = []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo"},{"id":"YmFyYmFy","display_name":"bar"}]}`)
}
require.NoError(t, fix.reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i))
i.Credentials = map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {
Type: identity.CredentialsTypePassword,
Identifiers: []string{identifier},
Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`),
},
identity.CredentialsTypePasskey: {
Type: identity.CredentialsTypePasskey,
Identifiers: []string{identifier},
Config: conf,
},
}
require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(t.Context(), i))
return i, identifier
}
func (fix *fixture) createIdentity(t *testing.T) *identity.Identity {
id, _ := fix.createIdentityAndReturnIdentifier(t, nil)
return id
}