From f47ceeae3217a9ceb388fab5de12a6476f4f61ad Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Mon, 4 Dec 2023 12:02:09 +0100 Subject: [PATCH] test: add registration tests --- driver/config/config.go | 1 + selfservice/flow/login/sort.go | 1 + ...when_passwordless_is_disabled-browser.json | 80 +++ ...ist_when_passwordless_is_disabled-spa.json | 80 +++ ...on-case=passkey_button_exists-browser.json | 99 ++++ ...ration-case=passkey_button_exists-spa.json | 99 ++++ .../success/internal_context.json | 3 +- selfservice/strategy/passkey/passkey_login.go | 13 +- .../strategy/passkey/passkey_registration.go | 16 +- .../passkey/passkey_registration_test.go | 526 ++++++++++++++++++ .../strategy/passkey/stub/login.schema.json | 11 + .../stub/missing-identifier.schema.json | 21 + .../strategy/passkey/stub/noid.schema.json | 18 + .../strategy/passkey/stub/profile.schema.json | 25 + .../passkey/stub/registration.schema.json | 35 ++ .../passkey/stub/settings.schema.json | 31 ++ ...oad_is_set_when_identity_has_webauthn.json | 2 +- ...ebauthn_login_is_invalid-type=browser.json | 2 +- ...if_webauthn_login_is_invalid-type=spa.json | 2 +- ...passwordless_enabled=false#01-browser.json | 2 +- ...als-passwordless_enabled=false#01-spa.json | 2 +- ...passwordless_enabled=false#02-browser.json | 2 +- ...als-passwordless_enabled=false#02-spa.json | 2 +- ...ls-passwordless_enabled=false-browser.json | 2 +- ...ntials-passwordless_enabled=false-spa.json | 2 +- ...-passwordless_enabled=true#01-browser.json | 2 +- ...ials-passwordless_enabled=true#01-spa.json | 2 +- ...-passwordless_enabled=true#02-browser.json | 2 +- ...ials-passwordless_enabled=true#02-spa.json | 2 +- ...als-passwordless_enabled=true-browser.json | 2 +- ...entials-passwordless_enabled=true-spa.json | 2 +- ...device_is_shown_which_can_be_unlinked.json | 2 +- ...-case=one_activation_element_is_shown.json | 2 +- ...n-case=webauthn_button_exists-browser.json | 2 +- ...ation-case=webauthn_button_exists-spa.json | 2 +- ui/node/identifiers.go | 4 +- x/webauthnx/js/webauthn.js | 75 ++- 37 files changed, 1130 insertions(+), 46 deletions(-) create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json create mode 100644 selfservice/strategy/passkey/passkey_registration_test.go create mode 100644 selfservice/strategy/passkey/stub/login.schema.json create mode 100644 selfservice/strategy/passkey/stub/missing-identifier.schema.json create mode 100644 selfservice/strategy/passkey/stub/noid.schema.json create mode 100644 selfservice/strategy/passkey/stub/profile.schema.json create mode 100644 selfservice/strategy/passkey/stub/registration.schema.json create mode 100644 selfservice/strategy/passkey/stub/settings.schema.json diff --git a/driver/config/config.go b/driver/config/config.go index fda46518c0b9..98055b88ffa2 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -183,6 +183,7 @@ const ( ViperKeyWebAuthnRPOrigin = "selfservice.methods.webauthn.config.rp.origin" ViperKeyWebAuthnRPOrigins = "selfservice.methods.webauthn.config.rp.origins" ViperKeyWebAuthnPasswordless = "selfservice.methods.webauthn.config.passwordless" + ViperKeyPasskeyEnabled = "selfservice.methods.passkey.enabled" ViperKeyPasskeyRPDisplayName = "selfservice.methods.passkey.config.rp.display_name" ViperKeyPasskeyRPID = "selfservice.methods.passkey.config.rp.id" ViperKeyPasskeyRPOrigins = "selfservice.methods.passkey.config.rp.origins" diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go index 9f1a144ffc2d..c9d9145d46e3 100644 --- a/selfservice/flow/login/sort.go +++ b/selfservice/flow/login/sort.go @@ -15,6 +15,7 @@ func sortNodes(ctx context.Context, n node.Nodes) error { node.OpenIDConnectGroup, node.DefaultGroup, node.WebAuthnGroup, + node.PasskeyGroup, node.CodeGroup, node.PasswordGroup, node.TOTPGroup, diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json new file mode 100644 index 000000000000..cd42a6256ce0 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json @@ -0,0 +1,80 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json new file mode 100644 index 000000000000..cd42a6256ce0 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json @@ -0,0 +1,80 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json new file mode 100644 index 000000000000..103430a69d76 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -0,0 +1,99 @@ +[ + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "passkey" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json new file mode 100644 index 000000000000..103430a69d76 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -0,0 +1,99 @@ +[ + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "passkey" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/fixtures/registration/success/internal_context.json b/selfservice/strategy/passkey/fixtures/registration/success/internal_context.json index b986cdab0dee..c4c2e93e7b8e 100644 --- a/selfservice/strategy/passkey/fixtures/registration/success/internal_context.json +++ b/selfservice/strategy/passkey/fixtures/registration/success/internal_context.json @@ -1,6 +1,5 @@ { - "totp_url": "otpauth://totp/issuer.ory.sh:6e11a9a7-62fd-4c88-871a-097f18f0306f?algorithm=SHA1&digits=6&issuer=issuer.ory.sh&period=30&secret=2F43HRJNMUW67EDMRR7AKQYRZP3AI6IG", - "webauthn_session_data": { + "passkey_session_data": { "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", "user_id": "bhGpp2L9TIiHGgl/GPAwbw==", "userVerification": "" diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index c8bcbdd1bd41..c1f630905c64 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -50,7 +50,8 @@ func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, loginFlow ctx := r.Context() loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - loginFlow.UI.SetNode(node.NewInputField( + + loginFlow.UI.Nodes.Upsert(node.NewInputField( "identifier", "", node.DefaultGroup, @@ -84,7 +85,7 @@ func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, loginFlow loginFlow.UI.Nodes.Upsert(&node.Node{ Type: node.Input, - Group: node.WebAuthnGroup, + Group: node.PasskeyGroup, Meta: &node.Meta{}, Attributes: &node.InputAttributes{ Name: "passkey_challenge", @@ -104,10 +105,10 @@ func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, loginFlow }}) loginFlow.UI.Nodes.Append(node.NewInputField( - "method", - "passkey", - node.WebAuthnGroup, - node.InputAttributeTypeSubmit, + "login_with_passkey", + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) return nil diff --git a/selfservice/strategy/passkey/passkey_registration.go b/selfservice/strategy/passkey/passkey_registration.go index 4cc1183709ee..58882d0d8ffe 100644 --- a/selfservice/strategy/passkey/passkey_registration.go +++ b/selfservice/strategy/passkey/passkey_registration.go @@ -53,7 +53,7 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registra regFlow.UI.Nodes.Append(node.NewInputField( "method", "passkey", - node.WebAuthnGroup, + node.PasskeyGroup, node.InputAttributeTypeSubmit, ).WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterPasskey())) @@ -95,7 +95,7 @@ func (s *Strategy) identifierNode(ctx context.Context) (*node.Node, error) { } } - return nil, errors.New("identifier node not found") + return nil, schema.NewMissingIdentifierError() } func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, ident *identity.Identity) (err error) { @@ -199,9 +199,6 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *reg func (s *Strategy) createPasskey(r *http.Request, w http.ResponseWriter, regFlow *registration.Flow, params *updateRegistrationFlowWithPasskeyMethod) error { ctx := r.Context() - //regFlow.UI.Nodes = node.Nodes{} - //regFlow.UI.Messages.Clear() - idNode, err := s.identifierNode(ctx) if err != nil { return s.handleRegistrationError(w, r, regFlow, params, err) @@ -214,11 +211,7 @@ func (s *Strategy) createPasskey(r *http.Request, w http.ResponseWriter, regFlow } var identifier string for _, n := range c.Nodes { - //if attr, ok := n.Attributes.(*node.InputAttributes); ok { - // attr.Type = node.InputAttributeTypeHidden - //} regFlow.UI.SetValue(n.ID(), n) - //regFlow.UI.SetNode(n) if n.ID() == idNode.ID() { identifier, _ = n.Attributes.GetValue().(string) } @@ -261,7 +254,7 @@ func (s *Strategy) createPasskey(r *http.Request, w http.ResponseWriter, regFlow Group: node.PasskeyGroup, Meta: &node.Meta{}, Attributes: &node.InputAttributes{ - Name: "passkey_register", + Name: node.PasskeyRegister, Type: node.InputAttributeTypeHidden, }}) @@ -270,7 +263,7 @@ func (s *Strategy) createPasskey(r *http.Request, w http.ResponseWriter, regFlow Group: node.WebAuthnGroup, Meta: &node.Meta{}, Attributes: &node.InputAttributes{ - Name: "create_passkey_data", + Name: node.PasskeyCreateData, Type: node.InputAttributeTypeHidden, FieldValue: string(injectWebAuthnOptions), }}) @@ -337,7 +330,6 @@ func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Reques } } - //f.UI.Nodes.SetValueAttribute(node.WebAuthnRegisterDisplayName, p.RegisterDisplayName) if f.Type == flow.TypeBrowser { f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) } diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go new file mode 100644 index 000000000000..67c50becdfdc --- /dev/null +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -0,0 +1,526 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + _ "embed" + "encoding/base64" + "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/tidwall/sjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" + "github.com/ory/kratos/internal/registrationhelpers" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/passkey" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/randx" +) + +var ( + flows = []string{"spa", "browser"} + + //go:embed fixtures/registration/success/response.json + registrationFixtureSuccessResponse []byte + //go:embed fixtures/registration/success/internal_context.json + registrationFixtureSuccessInternalContext []byte +) + +func flowIsSPA(flow string) bool { + return flow == "spa" +} + +func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) + enablePasskeyStrategy(conf) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + return reg +} + +func enablePasskeyStrategy(conf *config.Config) { + ctx := context.Background() + key := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePasskey) + conf.MustSet(ctx, key+".enabled", true) + conf.MustSet(ctx, key+".config.rp.display_name", "Ory Corp") + conf.MustSet(ctx, key+".config.rp.id", "localhost") + conf.MustSet(ctx, key+".config.rp.origins", []string{"http://localhost:4455"}) +} + +type fixture struct { + ctx context.Context + conf *config.Config + reg *driver.RegistryDefault + publicTS *httptest.Server + redirTS *httptest.Server + redirNoSessionTS *httptest.Server +} + +func setup(t *testing.T) *fixture { + f := new(fixture) + f.ctx = context.Background() + f.reg = newRegistrationRegistry(t) + f.conf = f.reg.Config() + ctx := context.Background() + + router := x.NewRouterPublic() + f.publicTS, _ = testhelpers.NewKratosServerWithRouters(t, f.reg, router, x.NewRouterAdmin()) + + _ = testhelpers.NewErrorTestServer(t, f.reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, f.reg) + _ = testhelpers.NewRedirSessionEchoTS(t, f.reg) + + testhelpers.SetDefaultIdentitySchema(f.conf, "file://./stub/registration.schema.json") + f.conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + + f.redirTS = testhelpers.NewRedirSessionEchoTS(t, f.reg) + f.redirNoSessionTS = testhelpers.NewRedirNoSessionTS(t, f.reg) + + f.useReturnToFromTS(f.redirTS) + + return f +} + +// 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(fix.ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to") + fix.conf.MustSet(fix.ctx, 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(fix.ctx, config.HookStrategyKey( + config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypePasskey.String(), + ), nil) +} +func (fix *fixture) enableSessionAfterRegistration() { + fix.conf.MustSet(fix.ctx, config.HookStrategyKey( + config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypePasskey.String(), + ), []config.SelfServiceHook{{Name: "session"}}) +} + +type submitPasskeyOpt struct { + initFlowOpts []testhelpers.InitFlowWithOption + userID string +} + +type submitPasskeyOption func(o *submitPasskeyOpt) + +func withInitFlowOpt(opt testhelpers.InitFlowWithOption) submitPasskeyOption { + return func(o *submitPasskeyOpt) { + o.initFlowOpts = append(o.initFlowOpts, opt) + } +} +func withUserID(id string) submitPasskeyOption { + return func(o *submitPasskeyOpt) { + o.userID = base64.StdEncoding.EncodeToString([]byte(id)) + } +} + +func (fix *fixture) submitPasskeyRegistration( + t *testing.T, + flowType string, + client *http.Client, + cb func(values url.Values), + opts ...submitPasskeyOption, +) (string, *http.Response, *kratos.RegistrationFlow) { + o := new(submitPasskeyOpt) + for _, fn := range opts { + fn(o) + } + + isSPA := flowType == "spa" + regFlow := testhelpers.InitializeRegistrationFlowViaBrowser(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") + body, _ := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, values.Encode()) + + // We inject the session to replay + interim, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(fix.ctx, uuid.FromStringOrNil(regFlow.Id)) + require.NoError(t, err) + interim.InternalContext = registrationFixtureSuccessInternalContext + 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(fix.ctx, interim)) + + // Second step: fill out passkey response + values.Set(node.PasskeyRegister, passkeyRegisterVal) + body, res := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, values.Encode()) + + 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.submitPasskeyRegistration(t, flowType, testhelpers.NewClientWithCookies(t), values, opts...) + fetchedFlow, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(fix.ctx, 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, fetchedFlow := fix.makeRegistration(t, flowType, values, opts...) + assert.Empty(t, gjson.GetBytes( + fetchedFlow.InternalContext, + flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData)), + "has cleaned up the internal context after success") + if flowType == "spa" { + expectReturnTo = fix.publicTS.URL + } + assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual)) + return actual +} + +func TestRegistration(t *testing.T) { + t.Parallel() + + t.Run("AssertCommonErrorCases", func(t *testing.T) { + registrationhelpers.AssertCommonErrorCases(t, flows) + }) + + t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) { + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertCSRFFailures", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) { + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertSchemaDoesNotExist", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertSchemDoesNotExist(t, reg, flows, func(v url.Values) { + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("case=passkey button does not exist when passwordless is disabled", func(t *testing.T) { + t.Parallel() + fix := setup(t) + fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyEnabled, false) + t.Cleanup(func() { fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyEnabled, true) }) + for _, flowType := range flows { + flowType := flowType + t.Run(flowType, func(t *testing.T) { + t.Parallel() + client := testhelpers.NewClientWithCookies(t) + flo := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, flowIsSPA(flowType), false, false) + testhelpers.SnapshotTExcept(t, flo.Ui.Nodes, []string{ + "0.attributes.value", + }) + }) + } + }) + + t.Run("case=passkey button exists", func(t *testing.T) { + t.Parallel() + fix := setup(t) + for _, flowType := range flows { + flowType := flowType + t.Run(flowType, func(t *testing.T) { + t.Parallel() + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, flowIsSPA(flowType), false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "3.attributes.value", + }) + }) + } + }) + + t.Run("case=should return an error because not passing validation", func(t *testing.T) { + t.Parallel() + fix := setup(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t, fix.publicTS, fix.conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "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) { + t.Parallel() + fix := setup(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bar") + v.Set("transient_payload", "42") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t, fix.publicTS, fix.conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "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 passkey response is invalid", func(t *testing.T) { + t.Parallel() + fix := setup(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, "invalid") + v.Set("method", "passkey") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := fix.submitPasskeyRegistration(t, f, testhelpers.NewClientWithCookies(t), values) + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), node.PasskeyRegister, "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) + }) + } + }) + + t.Run("case=should fail to create identity if schema is missing the identifier", func(t *testing.T) { + t.Parallel() + fix := setup(t) + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/noid.schema.json") + email := testhelpers.RandomEmail() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + isSPA := f == "spa" + regFlow := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, isSPA, false, false) + + // fill out traits and click on "sign up with passkey" + urlValues := testhelpers.SDKFormFieldsToURLValues(regFlow.Ui.Nodes) + urlValues.Set("traits.email", email) + urlValues.Set("method", "passkey") + actual, _ := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, urlValues.Encode()) + + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.email") + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + getPrefix := func(f string) (prefix string) { + if f == "spa" { + prefix = "session." + } + return + } + + t.Run("successful registration", func(t *testing.T) { + t.Parallel() + fix := setup(t) + t.Cleanup(fix.disableSessionAfterRegistration) + + var 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.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + } + + t.Run("case=should create the identity but not a session", func(t *testing.T) { + fix.useRedirNoSessionTS() + t.Cleanup(fix.useRedirTS) + fix.disableSessionAfterRegistration() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := f + "-" + testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirNoSessionTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + + 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 := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + + t.Run("case=should accept valid transient payload", func(t *testing.T) { + fix.useRedirNoSessionTS() + t.Cleanup(fix.useRedirTS) + fix.disableSessionAfterRegistration() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirNoSessionTS.URL+"/registration-return-ts", func(v url.Values) { + values(email)(v) + v.Set("transient_payload.stuff", "42") + }, withUserID(userID)) + + 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 := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + 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) { + fix.enableSessionAfterRegistration() + fix.conf.MustSet(fix.ctx, config.ViperKeyDefaultIdentitySchemaID, "advanced-user") + fix.conf.MustSet(fix.ctx, 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() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + + 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 := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + 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) { + fix.enableSessionAfterRegistration() + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + assert.True(t, gjson.Get(actual, getPrefix(f)+"active").Bool(), "%s", actual) + + actual, _, _ = fix.makeRegistration(t, f, values(email)) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "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 password.", + gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + t.Run("case=reset previous form errors", func(t *testing.T) { + fix.enableSessionAfterRegistration() + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + actual, _, _ := fix.makeRegistration(t, f, func(v url.Values) { + v.Del("traits.username") + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + }) + registrationhelpers.CheckFormContent(t, []byte(actual), "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, _, _ = fix.makeRegistration(t, f, func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + }) + registrationhelpers.CheckFormContent(t, []byte(actual), "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()) + }) + } + }) + }) +} diff --git a/selfservice/strategy/passkey/stub/login.schema.json b/selfservice/strategy/passkey/stub/login.schema.json new file mode 100644 index 000000000000..82f85811a16a --- /dev/null +++ b/selfservice/strategy/passkey/stub/login.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object" + } + } +} diff --git a/selfservice/strategy/passkey/stub/missing-identifier.schema.json b/selfservice/strategy/passkey/stub/missing-identifier.schema.json new file mode 100644 index 000000000000..43565799bba7 --- /dev/null +++ b/selfservice/strategy/passkey/stub/missing-identifier.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/noid.schema.json b/selfservice/strategy/passkey/stub/noid.schema.json new file mode 100644 index 000000000000..d1dcaa77d138 --- /dev/null +++ b/selfservice/strategy/passkey/stub/noid.schema.json @@ -0,0 +1,18 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/profile.schema.json b/selfservice/strategy/passkey/stub/profile.schema.json new file mode 100644 index 000000000000..2503b33597d7 --- /dev/null +++ b/selfservice/strategy/passkey/stub/profile.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "passkey": { + "identifier": true + } + } + } + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/registration.schema.json b/selfservice/strategy/passkey/stub/registration.schema.json new file mode 100644 index 000000000000..0b52f2a133af --- /dev/null +++ b/selfservice/strategy/passkey/stub/registration.schema.json @@ -0,0 +1,35 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "foobar": { + "type": "string", + "minLength": 2 + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "required": ["foobar", "username"] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/settings.schema.json b/selfservice/strategy/passkey/stub/settings.schema.json new file mode 100644 index 000000000000..6f137de72167 --- /dev/null +++ b/selfservice/strategy/passkey/stub/settings.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "website": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "identifier": true + } + } + } + } + }, + "required": [] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index 891ed604a787..6e7e56818210 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -61,7 +61,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index b3622087d5a6..ca84818c7889 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index b3622087d5a6..ca84818c7889 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index 78585438add5..d2d423612f11 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 0bc243b04581..22cfc24605a3 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -116,7 +116,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 398d92edf9f6..5767a50657d9 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -68,7 +68,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index 0be7a426eda7..71a6053e8542 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index 0be7a426eda7..71a6053e8542 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-rrszHygyJaBBmm2DANDUIW2BnuquPAiOn/kNlVc6joA+O3H9N8CS5qHEL+HDn6DAVJnBeqrUQWlX5kmt5j1qjw==", + "integrity": "sha512-YzOFTy1y50T/3vPo0kEOcZASjzaclfJEHdP52e8Tq5g2n8B9ybVa5VdtX1YBBysvFluHihEzXoSFpEF/mRtBKQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/ui/node/identifiers.go b/ui/node/identifiers.go index 75431648c020..ae8a77c6fbf6 100644 --- a/ui/node/identifiers.go +++ b/ui/node/identifiers.go @@ -28,6 +28,6 @@ const ( WebAuthnRemove = "webauthn_remove" WebAuthnScript = "webauthn_script" PasskeyRegisterTrigger = "passkey_register_trigger" - CreatePasskey = "create_passkey" - LoginPasskey = "login_passkey" + PasskeyRegister = "passkey_register" + PasskeyCreateData = "create_passkey_data" ) diff --git a/x/webauthnx/js/webauthn.js b/x/webauthnx/js/webauthn.js index df71ccfb142a..78251b13a6f0 100644 --- a/x/webauthnx/js/webauthn.js +++ b/x/webauthnx/js/webauthn.js @@ -125,13 +125,13 @@ }) } - async function __oryPasskeyLogin() { + async function __oryPasskeyLoginAutocomplete() { const dataEl = document.getElementsByName("passkey_challenge")[0] const resultEl = document.getElementsByName("passkey_login")[0] const identifierEl = document.getElementsByName("identifier")[0] if (!dataEl || !resultEl || !identifierEl) { - console.debug("__oryPasskeyLogin: mandatory fields not found") + console.debug("__oryPasskeyLoginAutocomplete: mandatory fields not found") return } @@ -157,10 +157,14 @@ } opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + // Allow aborting through a global variable + window.abortPasskeyConditionalUI = new AbortController() + navigator.credentials .get({ publicKey: opt.publicKey, mediation: "conditional", + signal: abortPasskeyConditionalUI.signal, }) .then(function (credential) { resultEl.value = JSON.stringify({ @@ -180,14 +184,70 @@ ), }, }) - identifierEl.value = credential.id document .querySelector('*[type="submit"][name="method"][value="passkey"]') .click() }) .catch((err) => { - alert(err) + console.log(err) + }) + } + + async function __oryPasskeyLogin() { + const dataEl = document.getElementsByName("passkey_challenge")[0] + const resultEl = document.getElementsByName("passkey_login")[0] + + if (!dataEl || !resultEl) { + console.debug("__oryPasskeyLogin: mandatory fields not found") + return + } + if (!window.PublicKeyCredential) { + console.log("This browser does not support WebAuthn!") + return + } + + let opt = JSON.parse(dataEl.value) + + if (opt.publicKey.user && opt.publicKey.user.id) { + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + } + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + + window.abortPasskeyConditionalUI && + window.abortPasskeyConditionalUI.abort( + "only one credentials.get allowed at a time", + ) + + navigator.credentials + .get({ + publicKey: opt.publicKey, + }) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + authenticatorData: __oryWebAuthnBufferEncode( + credential.response.authenticatorData, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + signature: __oryWebAuthnBufferEncode(credential.response.signature), + userHandle: __oryWebAuthnBufferEncode( + credential.response.userHandle, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + // Calling this again will enable the autocomplete once again. + __oryPasskeyLoginAutocomplete() + console.error(err) }) } @@ -297,13 +357,18 @@ }) } - document.addEventListener("DOMContentLoaded", __oryPasskeyLogin) + document.addEventListener("DOMContentLoaded", __oryPasskeyLoginAutocomplete) document.addEventListener("DOMContentLoaded", __oryPasskeyRegistration) document.addEventListener("DOMContentLoaded", function () { for (const el of document.getElementsByName("passkey_register_trigger")) { el.addEventListener("click", __oryPasskeySettingsRegistration) } }) + document.addEventListener("DOMContentLoaded", function () { + for (const el of document.getElementsByName("login_with_passkey")) { + el.addEventListener("click", __oryPasskeyLogin) + } + }) window.__oryWebAuthnLogin = __oryWebAuthnLogin window.__oryWebAuthnRegistration = __oryWebAuthnRegistration