diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9c2ee0555b42..596977093199 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -289,6 +289,7 @@ jobs:
uses: actions/checkout@v3
with:
repository: ory/kratos-selfservice-ui-node
+ ref: jonas-jonas/two-step
path: node-ui
- run: |
cd node-ui
diff --git a/.gitignore b/.gitignore
index f792761b2b71..42d798e427ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,3 +65,4 @@ test/e2e/kratos.*.yml
# VSCode debug artifact
__debug_bin
.debug.sqlite.db
+.last-run.json
\ No newline at end of file
diff --git a/.schema/openapi/patches/schema.yaml b/.schema/openapi/patches/schema.yaml
index 206aceb2708e..ff661ce4079d 100644
--- a/.schema/openapi/patches/schema.yaml
+++ b/.schema/openapi/patches/schema.yaml
@@ -43,6 +43,7 @@
set_ory_session_token: "#/components/schemas/continueWithSetOrySessionToken"
show_settings_ui: "#/components/schemas/continueWithSettingsUi"
show_recovery_ui: "#/components/schemas/continueWithRecoveryUi"
+ redirect_browser_to: "#/components/schemas/continueWithRedirectBrowserTo"
- op: add
path: /components/schemas/continueWith/oneOf
@@ -51,3 +52,4 @@
- "$ref": "#/components/schemas/continueWithSetOrySessionToken"
- "$ref": "#/components/schemas/continueWithSettingsUi"
- "$ref": "#/components/schemas/continueWithRecoveryUi"
+ - "$ref": "#/components/schemas/continueWithRedirectBrowserTo"
diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml
index 81d82247586b..7887c1c2da74 100644
--- a/.schema/openapi/patches/selfservice.yaml
+++ b/.schema/openapi/patches/selfservice.yaml
@@ -19,6 +19,7 @@
- "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod"
- "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod"
- "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ - "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod"
- op: add
path: /components/schemas/updateRegistrationFlowBody/discriminator
value:
@@ -28,7 +29,8 @@
oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod"
webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod"
code: "#/components/schemas/updateRegistrationFlowWithCodeMethod"
- passKey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ passkey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ profile: "#/components/schemas/updateRegistrationFlowWithProfileMethod"
- op: add
path: /components/schemas/registrationFlowState/enum
value:
@@ -50,6 +52,7 @@
- "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod"
- "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod"
- "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod"
+ - "$ref": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod"
- op: add
path: /components/schemas/updateLoginFlowBody/discriminator
value:
@@ -62,6 +65,7 @@
lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod"
code: "#/components/schemas/updateLoginFlowWithCodeMethod"
passkey: "#/components/schemas/updateLoginFlowWithPasskeyMethod"
+ identifier_first: "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod"
- op: add
path: /components/schemas/loginFlowState/enum
value:
diff --git a/Makefile b/Makefile
index 7af282d469c3..0395085b9849 100644
--- a/Makefile
+++ b/Makefile
@@ -193,8 +193,8 @@ migrations-sync: .bin/ory
ory dev pop migration sync persistence/sql/migrations/templates persistence/sql/migratest/testdata
script/add-down-migrations.sh
-.PHONY: test-update-snapshots
-test-update-snapshots:
+.PHONY: test-refresh
+test-refresh:
UPDATE_SNAPSHOTS=true go test -tags sqlite,json1,refresh -short ./...
.PHONY: post-release
diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go
index 6ed8df8d1748..a498aa9dfca6 100644
--- a/cmd/clidoc/main.go
+++ b/cmd/clidoc/main.go
@@ -177,6 +177,8 @@ func init() {
"NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(),
"NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(),
"NewInfoSelfServiceLoginCodeMFAHint": text.NewInfoSelfServiceLoginCodeMFAHint("{maskedIdentifier}"),
+ "NewInfoLoginPassword": text.NewInfoLoginPassword(),
+ "NewErrorValidationAccountNotFound": text.NewErrorValidationAccountNotFound(),
}
}
diff --git a/courier/sms_test.go b/courier/sms_test.go
index a93a7974bf71..5dc727048be6 100644
--- a/courier/sms_test.go
+++ b/courier/sms_test.go
@@ -63,6 +63,7 @@ func TestQueueSMS(t *testing.T) {
Body: body.Body,
})
}))
+ t.Cleanup(srv.Close)
requestConfig := fmt.Sprintf(`{
"url": "%s",
@@ -112,8 +113,6 @@ func TestQueueSMS(t *testing.T) {
assert.Equal(t, expected.To, message.To)
assert.Equal(t, fmt.Sprintf("stub sms body %s\n", expected.Body), message.Body)
}
-
- srv.Close()
}
func TestDisallowedInternalNetwork(t *testing.T) {
diff --git a/driver/config/config.go b/driver/config/config.go
index 81be527a2632..0c5b11ee0175 100644
--- a/driver/config/config.go
+++ b/driver/config/config.go
@@ -134,6 +134,8 @@ const (
ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after"
ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.flows.registration.before.hooks"
ViperKeySelfServiceLoginUI = "selfservice.flows.login.ui_url"
+ ViperKeySelfServiceLoginFlowStyle = "selfservice.flows.login.style"
+ ViperKeySecurityAccountEnumerationMitigate = "security.account_enumeration.mitigate"
ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan"
ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after"
ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks"
@@ -774,7 +776,7 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self
var err error
config, err = json.Marshal(pp.GetF(basePath+".config", config))
if err != nil {
- p.l.WithError(err).Warn("Unable to marshal self service strategy configuration.")
+ p.l.WithError(err).Warn("Unable to marshal self-service strategy configuration.")
config = json.RawMessage("{}")
}
@@ -782,6 +784,8 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self
// we need to forcibly set these values here:
defaultEnabled := false
switch strategy {
+ case "identifier_first":
+ defaultEnabled = p.SelfServiceLoginFlowIdentifierFirstEnabled(ctx)
case "code", "password", "profile":
defaultEnabled = true
}
@@ -1612,3 +1616,16 @@ func (p *Config) PasswordMigrationHook(ctx context.Context) (hook *PasswordMigra
return hook
}
+
+func (p *Config) SelfServiceLoginFlowIdentifierFirstEnabled(ctx context.Context) bool {
+ switch p.GetProvider(ctx).String(ViperKeySelfServiceLoginFlowStyle) {
+ case "identifier_first":
+ return true
+ default:
+ return false
+ }
+}
+
+func (p *Config) SecurityAccountEnumerationMitigate(ctx context.Context) bool {
+ return p.GetProvider(ctx).Bool(ViperKeySecurityAccountEnumerationMitigate)
+}
diff --git a/driver/registry_default.go b/driver/registry_default.go
index eab63a120981..c77ab5d783f2 100644
--- a/driver/registry_default.go
+++ b/driver/registry_default.go
@@ -12,6 +12,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
"github.com/cenkalti/backoff"
"github.com/dgraph-io/ristretto"
"github.com/gobuffalo/pop/v6"
@@ -324,6 +326,7 @@ func (m *RegistryDefault) selfServiceStrategies() []any {
passkey.NewStrategy(m),
webauthn.NewStrategy(m),
lookup.NewStrategy(m),
+ idfirst.NewStrategy(m),
}
}
}
@@ -379,6 +382,7 @@ nextStrategy:
continue nextStrategy
}
}
+
if m.strategyLoginEnabled(ctx, s.ID().String()) {
loginStrategies = append(loginStrategies, s)
}
diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go
index fa3e7772c62a..a52b4fc6072c 100644
--- a/driver/registry_default_test.go
+++ b/driver/registry_default_test.go
@@ -872,7 +872,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) {
_, reg := internal.NewVeryFastRegistryWithoutDB(t)
t.Run("case=all login strategies", func(t *testing.T) {
- expects := []string{"password", "oidc", "code", "totp", "passkey", "webauthn", "lookup_secret"}
+ expects := []string{"password", "oidc", "code", "totp", "passkey", "webauthn", "lookup_secret", "identifier_first"}
s := reg.AllLoginStrategies()
require.Len(t, s, len(expects))
for k, e := range expects {
diff --git a/embedx/config.schema.json b/embedx/config.schema.json
index c62b3c39f00c..5016926ab036 100644
--- a/embedx/config.schema.json
+++ b/embedx/config.schema.json
@@ -1557,12 +1557,6 @@
"title": "Enables login flows code method to fulfil MFA requests",
"default": false
},
- "passwordless_login_fallback_enabled": {
- "type": "boolean",
- "title": "Passwordless Login Fallback Enabled",
- "description": "This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.",
- "default": false
- },
"enabled": {
"type": "boolean",
"title": "Enables Code Method",
@@ -2879,6 +2873,21 @@
}
}
},
+ "security": {
+ "type": "object",
+ "properties": {
+ "account_enumeration": {
+ "type": "object",
+ "properties": {
+ "mitigate": {
+ "type": "boolean",
+ "default": false,
+ "description": "Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet."
+ }
+ }
+ }
+ }
+ },
"version": {
"title": "The kratos version this config is written for.",
"description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.",
diff --git a/examples/go/pkg/common.go b/examples/go/pkg/common.go
index edb8c17e2e19..f39725103e33 100644
--- a/examples/go/pkg/common.go
+++ b/examples/go/pkg/common.go
@@ -11,11 +11,9 @@ import (
"os"
"testing"
- "github.com/ory/kratos/x"
-
- "github.com/ory/kratos/internal/testhelpers"
-
ory "github.com/ory/client-go"
+ "github.com/ory/kratos/internal/testhelpers"
+ "github.com/ory/kratos/x"
)
func PrintJSONPretty(v interface{}) {
diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES
index fdf34c5e1507..c573997505d8 100644
--- a/internal/client-go/.openapi-generator/FILES
+++ b/internal/client-go/.openapi-generator/FILES
@@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md
docs/ContinueWith.md
docs/ContinueWithRecoveryUi.md
docs/ContinueWithRecoveryUiFlow.md
+docs/ContinueWithRedirectBrowserTo.md
docs/ContinueWithSetOrySessionToken.md
docs/ContinueWithSettingsUi.md
docs/ContinueWithSettingsUiFlow.md
@@ -99,6 +100,7 @@ docs/UiText.md
docs/UpdateIdentityBody.md
docs/UpdateLoginFlowBody.md
docs/UpdateLoginFlowWithCodeMethod.md
+docs/UpdateLoginFlowWithIdentifierFirstMethod.md
docs/UpdateLoginFlowWithLookupSecretMethod.md
docs/UpdateLoginFlowWithOidcMethod.md
docs/UpdateLoginFlowWithPasskeyMethod.md
@@ -139,6 +141,7 @@ model_consistency_request_parameters.go
model_continue_with.go
model_continue_with_recovery_ui.go
model_continue_with_recovery_ui_flow.go
+model_continue_with_redirect_browser_to.go
model_continue_with_set_ory_session_token.go
model_continue_with_settings_ui.go
model_continue_with_settings_ui_flow.go
@@ -219,6 +222,7 @@ model_ui_text.go
model_update_identity_body.go
model_update_login_flow_body.go
model_update_login_flow_with_code_method.go
+model_update_login_flow_with_identifier_first_method.go
model_update_login_flow_with_lookup_secret_method.go
model_update_login_flow_with_oidc_method.go
model_update_login_flow_with_passkey_method.go
diff --git a/internal/client-go/README.md b/internal/client-go/README.md
index 04dd61ab7d1e..85af88a0d079 100644
--- a/internal/client-go/README.md
+++ b/internal/client-go/README.md
@@ -142,6 +142,7 @@ Class | Method | HTTP request | Description
- [ContinueWith](docs/ContinueWith.md)
- [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md)
- [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md)
+ - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md)
- [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md)
- [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md)
- [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md)
@@ -222,6 +223,7 @@ Class | Method | HTTP request | Description
- [UpdateIdentityBody](docs/UpdateIdentityBody.md)
- [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md)
- [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md)
+ - [UpdateLoginFlowWithIdentifierFirstMethod](docs/UpdateLoginFlowWithIdentifierFirstMethod.md)
- [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md)
- [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md)
- [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md)
diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum
index c966c8ddfd0d..6cc3f5911d11 100644
--- a/internal/client-go/go.sum
+++ b/internal/client-go/go.sum
@@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/internal/client-go/model_continue_with.go b/internal/client-go/model_continue_with.go
index 9e97dbf479e7..6fb1056836e6 100644
--- a/internal/client-go/model_continue_with.go
+++ b/internal/client-go/model_continue_with.go
@@ -19,6 +19,7 @@ import (
// ContinueWith - struct for ContinueWith
type ContinueWith struct {
ContinueWithRecoveryUi *ContinueWithRecoveryUi
+ ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo
ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken
ContinueWithSettingsUi *ContinueWithSettingsUi
ContinueWithVerificationUi *ContinueWithVerificationUi
@@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit
}
}
+// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith
+func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith {
+ return ContinueWith{
+ ContinueWithRedirectBrowserTo: v,
+ }
+}
+
// ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith
func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith {
return ContinueWith{
@@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.")
}
+ // check if the discriminator value is 'redirect_browser_to'
+ if jsonDict["action"] == "redirect_browser_to" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'set_ory_session_token'
if jsonDict["action"] == "set_ory_session_token" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'continueWithRedirectBrowserTo'
+ if jsonDict["action"] == "continueWithRedirectBrowserTo" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'continueWithSetOrySessionToken'
if jsonDict["action"] == "continueWithSetOrySessionToken" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.ContinueWithRecoveryUi)
}
+ if src.ContinueWithRedirectBrowserTo != nil {
+ return json.Marshal(&src.ContinueWithRedirectBrowserTo)
+ }
+
if src.ContinueWithSetOrySessionToken != nil {
return json.Marshal(&src.ContinueWithSetOrySessionToken)
}
@@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} {
return obj.ContinueWithRecoveryUi
}
+ if obj.ContinueWithRedirectBrowserTo != nil {
+ return obj.ContinueWithRedirectBrowserTo
+ }
+
if obj.ContinueWithSetOrySessionToken != nil {
return obj.ContinueWithSetOrySessionToken
}
diff --git a/internal/client-go/model_continue_with_recovery_ui_flow.go b/internal/client-go/model_continue_with_recovery_ui_flow.go
index 3fde7e717ef2..251725a73c3b 100644
--- a/internal/client-go/model_continue_with_recovery_ui_flow.go
+++ b/internal/client-go/model_continue_with_recovery_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithRecoveryUiFlow struct {
// The ID of the recovery flow
Id string `json:"id"`
- // The URL of the recovery flow
+ // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
}
diff --git a/internal/client-go/model_continue_with_redirect_browser_to.go b/internal/client-go/model_continue_with_redirect_browser_to.go
new file mode 100644
index 000000000000..20c3e4f3c562
--- /dev/null
+++ b/internal/client-go/model_continue_with_redirect_browser_to.go
@@ -0,0 +1,138 @@
+/*
+ * Ory Identities API
+ *
+ * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more.
+ *
+ * API version:
+ * Contact: office@ory.sh
+ */
+
+// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
+
+package client
+
+import (
+ "encoding/json"
+)
+
+// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui
+type ContinueWithRedirectBrowserTo struct {
+ // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString
+ Action string `json:"action"`
+ // The URL to redirect the browser to
+ RedirectBrowserTo string `json:"redirect_browser_to"`
+}
+
+// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ this.Action = action
+ this.RedirectBrowserTo = redirectBrowserTo
+ return &this
+}
+
+// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ return &this
+}
+
+// GetAction returns the Action field value
+func (o *ContinueWithRedirectBrowserTo) GetAction() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Action
+}
+
+// GetActionOk returns a tuple with the Action field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Action, true
+}
+
+// SetAction sets field value
+func (o *ContinueWithRedirectBrowserTo) SetAction(v string) {
+ o.Action = v
+}
+
+// GetRedirectBrowserTo returns the RedirectBrowserTo field value
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.RedirectBrowserTo
+}
+
+// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.RedirectBrowserTo, true
+}
+
+// SetRedirectBrowserTo sets field value
+func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) {
+ o.RedirectBrowserTo = v
+}
+
+func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["action"] = o.Action
+ }
+ if true {
+ toSerialize["redirect_browser_to"] = o.RedirectBrowserTo
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableContinueWithRedirectBrowserTo struct {
+ value *ContinueWithRedirectBrowserTo
+ isSet bool
+}
+
+func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo {
+ return v.value
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableContinueWithRedirectBrowserTo) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo {
+ return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true}
+}
+
+func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/client-go/model_continue_with_settings_ui_flow.go b/internal/client-go/model_continue_with_settings_ui_flow.go
index 4ccaf74ef1b8..d6e9b9441f99 100644
--- a/internal/client-go/model_continue_with_settings_ui_flow.go
+++ b/internal/client-go/model_continue_with_settings_ui_flow.go
@@ -19,6 +19,8 @@ import (
type ContinueWithSettingsUiFlow struct {
// The ID of the settings flow
Id string `json:"id"`
+ // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ Url *string `json:"url,omitempty"`
}
// NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object
@@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) {
o.Id = v
}
+// GetUrl returns the Url field value if set, zero value otherwise.
+func (o *ContinueWithSettingsUiFlow) GetUrl() string {
+ if o == nil || o.Url == nil {
+ var ret string
+ return ret
+ }
+ return *o.Url
+}
+
+// GetUrlOk returns a tuple with the Url field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) {
+ if o == nil || o.Url == nil {
+ return nil, false
+ }
+ return o.Url, true
+}
+
+// HasUrl returns a boolean if a field has been set.
+func (o *ContinueWithSettingsUiFlow) HasUrl() bool {
+ if o != nil && o.Url != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUrl gets a reference to the given string and assigns it to the Url field.
+func (o *ContinueWithSettingsUiFlow) SetUrl(v string) {
+ o.Url = &v
+}
+
func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if true {
toSerialize["id"] = o.Id
}
+ if o.Url != nil {
+ toSerialize["url"] = o.Url
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/client-go/model_continue_with_verification_ui_flow.go b/internal/client-go/model_continue_with_verification_ui_flow.go
index 8fdd4609cf93..3c73a0761339 100644
--- a/internal/client-go/model_continue_with_verification_ui_flow.go
+++ b/internal/client-go/model_continue_with_verification_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithVerificationUiFlow struct {
// The ID of the verification flow
Id string `json:"id"`
- // The URL of the verification flow
+ // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
// The address that should be verified in this flow
VerifiableAddress string `json:"verifiable_address"`
diff --git a/internal/client-go/model_ui_node.go b/internal/client-go/model_ui_node.go
index e73f3c5e37d8..3582d9e85f67 100644
--- a/internal/client-go/model_ui_node.go
+++ b/internal/client-go/model_ui_node.go
@@ -18,7 +18,7 @@ import (
// UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `` tag, or an `` but also `some plain text`.
type UiNode struct {
Attributes UiNodeAttributes `json:"attributes"`
- // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup
+ // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup identifier_first IdentifierFirstGroup
Group string `json:"group"`
Messages []UiText `json:"messages"`
Meta UiNodeMeta `json:"meta"`
diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go
index b373dda7ccfd..f8deff5d5417 100644
--- a/internal/client-go/model_ui_node_input_attributes.go
+++ b/internal/client-go/model_ui_node_input_attributes.go
@@ -22,14 +22,20 @@ type UiNodeInputAttributes struct {
// Sets the input's disabled field to true or false.
Disabled bool `json:"disabled"`
Label *UiText `json:"label,omitempty"`
+ // MaxLength may contain the input's maximum length.
+ Maxlength *int64 `json:"maxlength,omitempty"`
// The input's element name.
Name string `json:"name"`
// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script
NodeType string `json:"node_type"`
- // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn.
+ // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.
Onclick *string `json:"onclick,omitempty"`
- // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn.
+ // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnclickTrigger *string `json:"onclickTrigger,omitempty"`
+ // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.
Onload *string `json:"onload,omitempty"`
+ // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnloadTrigger *string `json:"onloadTrigger,omitempty"`
// The input's pattern.
Pattern *string `json:"pattern,omitempty"`
// Mark this input field as required.
@@ -149,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) {
o.Label = &v
}
+// GetMaxlength returns the Maxlength field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetMaxlength() int64 {
+ if o == nil || o.Maxlength == nil {
+ var ret int64
+ return ret
+ }
+ return *o.Maxlength
+}
+
+// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) {
+ if o == nil || o.Maxlength == nil {
+ return nil, false
+ }
+ return o.Maxlength, true
+}
+
+// HasMaxlength returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasMaxlength() bool {
+ if o != nil && o.Maxlength != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field.
+func (o *UiNodeInputAttributes) SetMaxlength(v int64) {
+ o.Maxlength = &v
+}
+
// GetName returns the Name field value
func (o *UiNodeInputAttributes) GetName() string {
if o == nil {
@@ -229,6 +267,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) {
o.Onclick = &v
}
+// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnclickTrigger() string {
+ if o == nil || o.OnclickTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnclickTrigger
+}
+
+// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) {
+ if o == nil || o.OnclickTrigger == nil {
+ return nil, false
+ }
+ return o.OnclickTrigger, true
+}
+
+// HasOnclickTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnclickTrigger() bool {
+ if o != nil && o.OnclickTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field.
+func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) {
+ o.OnclickTrigger = &v
+}
+
// GetOnload returns the Onload field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetOnload() string {
if o == nil || o.Onload == nil {
@@ -261,6 +331,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) {
o.Onload = &v
}
+// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnloadTrigger() string {
+ if o == nil || o.OnloadTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnloadTrigger
+}
+
+// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) {
+ if o == nil || o.OnloadTrigger == nil {
+ return nil, false
+ }
+ return o.OnloadTrigger, true
+}
+
+// HasOnloadTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnloadTrigger() bool {
+ if o != nil && o.OnloadTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field.
+func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) {
+ o.OnloadTrigger = &v
+}
+
// GetPattern returns the Pattern field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetPattern() string {
if o == nil || o.Pattern == nil {
@@ -393,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Label != nil {
toSerialize["label"] = o.Label
}
+ if o.Maxlength != nil {
+ toSerialize["maxlength"] = o.Maxlength
+ }
if true {
toSerialize["name"] = o.Name
}
@@ -402,9 +507,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Onclick != nil {
toSerialize["onclick"] = o.Onclick
}
+ if o.OnclickTrigger != nil {
+ toSerialize["onclickTrigger"] = o.OnclickTrigger
+ }
if o.Onload != nil {
toSerialize["onload"] = o.Onload
}
+ if o.OnloadTrigger != nil {
+ toSerialize["onloadTrigger"] = o.OnloadTrigger
+ }
if o.Pattern != nil {
toSerialize["pattern"] = o.Pattern
}
diff --git a/internal/client-go/model_update_login_flow_body.go b/internal/client-go/model_update_login_flow_body.go
index b8bb05734e3c..f0d79322c54f 100644
--- a/internal/client-go/model_update_login_flow_body.go
+++ b/internal/client-go/model_update_login_flow_body.go
@@ -18,13 +18,14 @@ import (
// UpdateLoginFlowBody - struct for UpdateLoginFlowBody
type UpdateLoginFlowBody struct {
- UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
- UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
- UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
- UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
- UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
- UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
- UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
+ UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
+ UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod
+ UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
+ UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
+ UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
+ UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
+ UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
+ UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
}
// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody
@@ -34,6 +35,13 @@ func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCo
}
}
+// UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithIdentifierFirstMethod wrapped in UpdateLoginFlowBody
+func UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithIdentifierFirstMethod) UpdateLoginFlowBody {
+ return UpdateLoginFlowBody{
+ UpdateLoginFlowWithIdentifierFirstMethod: v,
+ }
+}
+
// UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody
func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody {
return UpdateLoginFlowBody{
@@ -98,6 +106,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'identifier_first'
+ if jsonDict["method"] == "identifier_first" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'lookup_secret'
if jsonDict["method"] == "lookup_secret" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -182,6 +202,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateLoginFlowWithIdentifierFirstMethod'
+ if jsonDict["method"] == "updateLoginFlowWithIdentifierFirstMethod" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod'
if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -263,6 +295,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateLoginFlowWithCodeMethod)
}
+ if src.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod)
+ }
+
if src.UpdateLoginFlowWithLookupSecretMethod != nil {
return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod)
}
@@ -299,6 +335,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} {
return obj.UpdateLoginFlowWithCodeMethod
}
+ if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return obj.UpdateLoginFlowWithIdentifierFirstMethod
+ }
+
if obj.UpdateLoginFlowWithLookupSecretMethod != nil {
return obj.UpdateLoginFlowWithLookupSecretMethod
}
diff --git a/internal/client-go/model_update_login_flow_with_identifier_first_method.go b/internal/client-go/model_update_login_flow_with_identifier_first_method.go
new file mode 100644
index 000000000000..70cf8002990d
--- /dev/null
+++ b/internal/client-go/model_update_login_flow_with_identifier_first_method.go
@@ -0,0 +1,212 @@
+/*
+ * Ory Identities API
+ *
+ * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more.
+ *
+ * API version:
+ * Contact: office@ory.sh
+ */
+
+// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
+
+package client
+
+import (
+ "encoding/json"
+)
+
+// UpdateLoginFlowWithIdentifierFirstMethod Update Login Flow with Multi-Step Method
+type UpdateLoginFlowWithIdentifierFirstMethod struct {
+ // Sending the anti-csrf token is only required for browser login flows.
+ CsrfToken *string `json:"csrf_token,omitempty"`
+ // Identifier is the email or username of the user trying to log in.
+ Identifier string `json:"identifier"`
+ // Method should be set to \"password\" when logging in using the identifier and password strategy.
+ Method string `json:"method"`
+ // Transient data to pass along to any webhooks
+ TransientPayload map[string]interface{} `json:"transient_payload,omitempty"`
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethod instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewUpdateLoginFlowWithIdentifierFirstMethod(identifier string, method string) *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ this.Identifier = identifier
+ this.Method = method
+ return &this
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults() *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ return &this
+}
+
+// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfToken() string {
+ if o == nil || o.CsrfToken == nil {
+ var ret string
+ return ret
+ }
+ return *o.CsrfToken
+}
+
+// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfTokenOk() (*string, bool) {
+ if o == nil || o.CsrfToken == nil {
+ return nil, false
+ }
+ return o.CsrfToken, true
+}
+
+// HasCsrfToken returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasCsrfToken() bool {
+ if o != nil && o.CsrfToken != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetCsrfToken(v string) {
+ o.CsrfToken = &v
+}
+
+// GetIdentifier returns the Identifier field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifier() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Identifier
+}
+
+// GetIdentifierOk returns a tuple with the Identifier field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifierOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Identifier, true
+}
+
+// SetIdentifier sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetIdentifier(v string) {
+ o.Identifier = v
+}
+
+// GetMethod returns the Method field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethod() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Method
+}
+
+// GetMethodOk returns a tuple with the Method field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethodOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Method, true
+}
+
+// SetMethod sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetMethod(v string) {
+ o.Method = v
+}
+
+// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayload() map[string]interface{} {
+ if o == nil || o.TransientPayload == nil {
+ var ret map[string]interface{}
+ return ret
+ }
+ return o.TransientPayload
+}
+
+// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayloadOk() (map[string]interface{}, bool) {
+ if o == nil || o.TransientPayload == nil {
+ return nil, false
+ }
+ return o.TransientPayload, true
+}
+
+// HasTransientPayload returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool {
+ if o != nil && o.TransientPayload != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetTransientPayload(v map[string]interface{}) {
+ o.TransientPayload = v
+}
+
+func (o UpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if o.CsrfToken != nil {
+ toSerialize["csrf_token"] = o.CsrfToken
+ }
+ if true {
+ toSerialize["identifier"] = o.Identifier
+ }
+ if true {
+ toSerialize["method"] = o.Method
+ }
+ if o.TransientPayload != nil {
+ toSerialize["transient_payload"] = o.TransientPayload
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableUpdateLoginFlowWithIdentifierFirstMethod struct {
+ value *UpdateLoginFlowWithIdentifierFirstMethod
+ isSet bool
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) Get() *UpdateLoginFlowWithIdentifierFirstMethod {
+ return v.value
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Set(val *UpdateLoginFlowWithIdentifierFirstMethod) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableUpdateLoginFlowWithIdentifierFirstMethod(val *UpdateLoginFlowWithIdentifierFirstMethod) *NullableUpdateLoginFlowWithIdentifierFirstMethod {
+ return &NullableUpdateLoginFlowWithIdentifierFirstMethod{value: val, isSet: true}
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go
index 64374c620f8f..82a578cfc4d3 100644
--- a/internal/client-go/model_update_registration_flow_body.go
+++ b/internal/client-go/model_update_registration_flow_body.go
@@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct {
UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod
UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod
UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod
+ UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod
UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod
}
@@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd
}
}
+// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody
+func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody {
+ return UpdateRegistrationFlowBody{
+ UpdateRegistrationFlowWithProfileMethod: v,
+ }
+}
+
// UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody
func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody {
return UpdateRegistrationFlowBody{
@@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
- // check if the discriminator value is 'passKey'
- if jsonDict["method"] == "passKey" {
+ // check if the discriminator value is 'passkey'
+ if jsonDict["method"] == "passkey" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod
err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod)
if err == nil {
@@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'profile'
+ if jsonDict["method"] == "profile" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'webauthn'
if jsonDict["method"] == "webauthn" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod'
+ if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod'
if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod)
}
+ if src.UpdateRegistrationFlowWithProfileMethod != nil {
+ return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod)
+ }
+
if src.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod)
}
@@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} {
return obj.UpdateRegistrationFlowWithPasswordMethod
}
+ if obj.UpdateRegistrationFlowWithProfileMethod != nil {
+ return obj.UpdateRegistrationFlowWithProfileMethod
+ }
+
if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return obj.UpdateRegistrationFlowWithWebAuthnMethod
}
diff --git a/internal/driver.go b/internal/driver.go
index b95b2dc7c0a9..00c86108a397 100644
--- a/internal/driver.go
+++ b/internal/driver.go
@@ -53,6 +53,7 @@ func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier)
config.ViperKeyCourierSMTPURL: "smtp://foo:bar@baz.com/",
config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.sh/redirect-not-set",
config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"},
+ config.ViperKeySelfServiceLoginFlowStyle: "one_step",
}),
configx.SkipValidation(),
}, opts...)
diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES
index fdf34c5e1507..c573997505d8 100644
--- a/internal/httpclient/.openapi-generator/FILES
+++ b/internal/httpclient/.openapi-generator/FILES
@@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md
docs/ContinueWith.md
docs/ContinueWithRecoveryUi.md
docs/ContinueWithRecoveryUiFlow.md
+docs/ContinueWithRedirectBrowserTo.md
docs/ContinueWithSetOrySessionToken.md
docs/ContinueWithSettingsUi.md
docs/ContinueWithSettingsUiFlow.md
@@ -99,6 +100,7 @@ docs/UiText.md
docs/UpdateIdentityBody.md
docs/UpdateLoginFlowBody.md
docs/UpdateLoginFlowWithCodeMethod.md
+docs/UpdateLoginFlowWithIdentifierFirstMethod.md
docs/UpdateLoginFlowWithLookupSecretMethod.md
docs/UpdateLoginFlowWithOidcMethod.md
docs/UpdateLoginFlowWithPasskeyMethod.md
@@ -139,6 +141,7 @@ model_consistency_request_parameters.go
model_continue_with.go
model_continue_with_recovery_ui.go
model_continue_with_recovery_ui_flow.go
+model_continue_with_redirect_browser_to.go
model_continue_with_set_ory_session_token.go
model_continue_with_settings_ui.go
model_continue_with_settings_ui_flow.go
@@ -219,6 +222,7 @@ model_ui_text.go
model_update_identity_body.go
model_update_login_flow_body.go
model_update_login_flow_with_code_method.go
+model_update_login_flow_with_identifier_first_method.go
model_update_login_flow_with_lookup_secret_method.go
model_update_login_flow_with_oidc_method.go
model_update_login_flow_with_passkey_method.go
diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md
index 04dd61ab7d1e..85af88a0d079 100644
--- a/internal/httpclient/README.md
+++ b/internal/httpclient/README.md
@@ -142,6 +142,7 @@ Class | Method | HTTP request | Description
- [ContinueWith](docs/ContinueWith.md)
- [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md)
- [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md)
+ - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md)
- [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md)
- [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md)
- [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md)
@@ -222,6 +223,7 @@ Class | Method | HTTP request | Description
- [UpdateIdentityBody](docs/UpdateIdentityBody.md)
- [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md)
- [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md)
+ - [UpdateLoginFlowWithIdentifierFirstMethod](docs/UpdateLoginFlowWithIdentifierFirstMethod.md)
- [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md)
- [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md)
- [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md)
diff --git a/internal/httpclient/model_continue_with.go b/internal/httpclient/model_continue_with.go
index 9e97dbf479e7..6fb1056836e6 100644
--- a/internal/httpclient/model_continue_with.go
+++ b/internal/httpclient/model_continue_with.go
@@ -19,6 +19,7 @@ import (
// ContinueWith - struct for ContinueWith
type ContinueWith struct {
ContinueWithRecoveryUi *ContinueWithRecoveryUi
+ ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo
ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken
ContinueWithSettingsUi *ContinueWithSettingsUi
ContinueWithVerificationUi *ContinueWithVerificationUi
@@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit
}
}
+// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith
+func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith {
+ return ContinueWith{
+ ContinueWithRedirectBrowserTo: v,
+ }
+}
+
// ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith
func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith {
return ContinueWith{
@@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.")
}
+ // check if the discriminator value is 'redirect_browser_to'
+ if jsonDict["action"] == "redirect_browser_to" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'set_ory_session_token'
if jsonDict["action"] == "set_ory_session_token" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'continueWithRedirectBrowserTo'
+ if jsonDict["action"] == "continueWithRedirectBrowserTo" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'continueWithSetOrySessionToken'
if jsonDict["action"] == "continueWithSetOrySessionToken" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.ContinueWithRecoveryUi)
}
+ if src.ContinueWithRedirectBrowserTo != nil {
+ return json.Marshal(&src.ContinueWithRedirectBrowserTo)
+ }
+
if src.ContinueWithSetOrySessionToken != nil {
return json.Marshal(&src.ContinueWithSetOrySessionToken)
}
@@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} {
return obj.ContinueWithRecoveryUi
}
+ if obj.ContinueWithRedirectBrowserTo != nil {
+ return obj.ContinueWithRedirectBrowserTo
+ }
+
if obj.ContinueWithSetOrySessionToken != nil {
return obj.ContinueWithSetOrySessionToken
}
diff --git a/internal/httpclient/model_continue_with_recovery_ui_flow.go b/internal/httpclient/model_continue_with_recovery_ui_flow.go
index 3fde7e717ef2..251725a73c3b 100644
--- a/internal/httpclient/model_continue_with_recovery_ui_flow.go
+++ b/internal/httpclient/model_continue_with_recovery_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithRecoveryUiFlow struct {
// The ID of the recovery flow
Id string `json:"id"`
- // The URL of the recovery flow
+ // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
}
diff --git a/internal/httpclient/model_continue_with_redirect_browser_to.go b/internal/httpclient/model_continue_with_redirect_browser_to.go
new file mode 100644
index 000000000000..20c3e4f3c562
--- /dev/null
+++ b/internal/httpclient/model_continue_with_redirect_browser_to.go
@@ -0,0 +1,138 @@
+/*
+ * Ory Identities API
+ *
+ * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more.
+ *
+ * API version:
+ * Contact: office@ory.sh
+ */
+
+// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
+
+package client
+
+import (
+ "encoding/json"
+)
+
+// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui
+type ContinueWithRedirectBrowserTo struct {
+ // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString
+ Action string `json:"action"`
+ // The URL to redirect the browser to
+ RedirectBrowserTo string `json:"redirect_browser_to"`
+}
+
+// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ this.Action = action
+ this.RedirectBrowserTo = redirectBrowserTo
+ return &this
+}
+
+// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ return &this
+}
+
+// GetAction returns the Action field value
+func (o *ContinueWithRedirectBrowserTo) GetAction() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Action
+}
+
+// GetActionOk returns a tuple with the Action field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Action, true
+}
+
+// SetAction sets field value
+func (o *ContinueWithRedirectBrowserTo) SetAction(v string) {
+ o.Action = v
+}
+
+// GetRedirectBrowserTo returns the RedirectBrowserTo field value
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.RedirectBrowserTo
+}
+
+// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.RedirectBrowserTo, true
+}
+
+// SetRedirectBrowserTo sets field value
+func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) {
+ o.RedirectBrowserTo = v
+}
+
+func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["action"] = o.Action
+ }
+ if true {
+ toSerialize["redirect_browser_to"] = o.RedirectBrowserTo
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableContinueWithRedirectBrowserTo struct {
+ value *ContinueWithRedirectBrowserTo
+ isSet bool
+}
+
+func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo {
+ return v.value
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableContinueWithRedirectBrowserTo) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo {
+ return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true}
+}
+
+func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/httpclient/model_continue_with_settings_ui_flow.go b/internal/httpclient/model_continue_with_settings_ui_flow.go
index 4ccaf74ef1b8..d6e9b9441f99 100644
--- a/internal/httpclient/model_continue_with_settings_ui_flow.go
+++ b/internal/httpclient/model_continue_with_settings_ui_flow.go
@@ -19,6 +19,8 @@ import (
type ContinueWithSettingsUiFlow struct {
// The ID of the settings flow
Id string `json:"id"`
+ // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ Url *string `json:"url,omitempty"`
}
// NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object
@@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) {
o.Id = v
}
+// GetUrl returns the Url field value if set, zero value otherwise.
+func (o *ContinueWithSettingsUiFlow) GetUrl() string {
+ if o == nil || o.Url == nil {
+ var ret string
+ return ret
+ }
+ return *o.Url
+}
+
+// GetUrlOk returns a tuple with the Url field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) {
+ if o == nil || o.Url == nil {
+ return nil, false
+ }
+ return o.Url, true
+}
+
+// HasUrl returns a boolean if a field has been set.
+func (o *ContinueWithSettingsUiFlow) HasUrl() bool {
+ if o != nil && o.Url != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUrl gets a reference to the given string and assigns it to the Url field.
+func (o *ContinueWithSettingsUiFlow) SetUrl(v string) {
+ o.Url = &v
+}
+
func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if true {
toSerialize["id"] = o.Id
}
+ if o.Url != nil {
+ toSerialize["url"] = o.Url
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/httpclient/model_continue_with_verification_ui_flow.go b/internal/httpclient/model_continue_with_verification_ui_flow.go
index 8fdd4609cf93..3c73a0761339 100644
--- a/internal/httpclient/model_continue_with_verification_ui_flow.go
+++ b/internal/httpclient/model_continue_with_verification_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithVerificationUiFlow struct {
// The ID of the verification flow
Id string `json:"id"`
- // The URL of the verification flow
+ // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
// The address that should be verified in this flow
VerifiableAddress string `json:"verifiable_address"`
diff --git a/internal/httpclient/model_ui_node.go b/internal/httpclient/model_ui_node.go
index e73f3c5e37d8..3582d9e85f67 100644
--- a/internal/httpclient/model_ui_node.go
+++ b/internal/httpclient/model_ui_node.go
@@ -18,7 +18,7 @@ import (
// UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `` tag, or an `` but also `some plain text`.
type UiNode struct {
Attributes UiNodeAttributes `json:"attributes"`
- // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup
+ // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup identifier_first IdentifierFirstGroup
Group string `json:"group"`
Messages []UiText `json:"messages"`
Meta UiNodeMeta `json:"meta"`
diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go
index b373dda7ccfd..f8deff5d5417 100644
--- a/internal/httpclient/model_ui_node_input_attributes.go
+++ b/internal/httpclient/model_ui_node_input_attributes.go
@@ -22,14 +22,20 @@ type UiNodeInputAttributes struct {
// Sets the input's disabled field to true or false.
Disabled bool `json:"disabled"`
Label *UiText `json:"label,omitempty"`
+ // MaxLength may contain the input's maximum length.
+ Maxlength *int64 `json:"maxlength,omitempty"`
// The input's element name.
Name string `json:"name"`
// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script
NodeType string `json:"node_type"`
- // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn.
+ // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.
Onclick *string `json:"onclick,omitempty"`
- // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn.
+ // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnclickTrigger *string `json:"onclickTrigger,omitempty"`
+ // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.
Onload *string `json:"onload,omitempty"`
+ // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnloadTrigger *string `json:"onloadTrigger,omitempty"`
// The input's pattern.
Pattern *string `json:"pattern,omitempty"`
// Mark this input field as required.
@@ -149,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) {
o.Label = &v
}
+// GetMaxlength returns the Maxlength field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetMaxlength() int64 {
+ if o == nil || o.Maxlength == nil {
+ var ret int64
+ return ret
+ }
+ return *o.Maxlength
+}
+
+// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) {
+ if o == nil || o.Maxlength == nil {
+ return nil, false
+ }
+ return o.Maxlength, true
+}
+
+// HasMaxlength returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasMaxlength() bool {
+ if o != nil && o.Maxlength != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field.
+func (o *UiNodeInputAttributes) SetMaxlength(v int64) {
+ o.Maxlength = &v
+}
+
// GetName returns the Name field value
func (o *UiNodeInputAttributes) GetName() string {
if o == nil {
@@ -229,6 +267,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) {
o.Onclick = &v
}
+// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnclickTrigger() string {
+ if o == nil || o.OnclickTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnclickTrigger
+}
+
+// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) {
+ if o == nil || o.OnclickTrigger == nil {
+ return nil, false
+ }
+ return o.OnclickTrigger, true
+}
+
+// HasOnclickTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnclickTrigger() bool {
+ if o != nil && o.OnclickTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field.
+func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) {
+ o.OnclickTrigger = &v
+}
+
// GetOnload returns the Onload field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetOnload() string {
if o == nil || o.Onload == nil {
@@ -261,6 +331,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) {
o.Onload = &v
}
+// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnloadTrigger() string {
+ if o == nil || o.OnloadTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnloadTrigger
+}
+
+// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) {
+ if o == nil || o.OnloadTrigger == nil {
+ return nil, false
+ }
+ return o.OnloadTrigger, true
+}
+
+// HasOnloadTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnloadTrigger() bool {
+ if o != nil && o.OnloadTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field.
+func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) {
+ o.OnloadTrigger = &v
+}
+
// GetPattern returns the Pattern field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetPattern() string {
if o == nil || o.Pattern == nil {
@@ -393,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Label != nil {
toSerialize["label"] = o.Label
}
+ if o.Maxlength != nil {
+ toSerialize["maxlength"] = o.Maxlength
+ }
if true {
toSerialize["name"] = o.Name
}
@@ -402,9 +507,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Onclick != nil {
toSerialize["onclick"] = o.Onclick
}
+ if o.OnclickTrigger != nil {
+ toSerialize["onclickTrigger"] = o.OnclickTrigger
+ }
if o.Onload != nil {
toSerialize["onload"] = o.Onload
}
+ if o.OnloadTrigger != nil {
+ toSerialize["onloadTrigger"] = o.OnloadTrigger
+ }
if o.Pattern != nil {
toSerialize["pattern"] = o.Pattern
}
diff --git a/internal/httpclient/model_update_login_flow_body.go b/internal/httpclient/model_update_login_flow_body.go
index b8bb05734e3c..f0d79322c54f 100644
--- a/internal/httpclient/model_update_login_flow_body.go
+++ b/internal/httpclient/model_update_login_flow_body.go
@@ -18,13 +18,14 @@ import (
// UpdateLoginFlowBody - struct for UpdateLoginFlowBody
type UpdateLoginFlowBody struct {
- UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
- UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
- UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
- UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
- UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
- UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
- UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
+ UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
+ UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod
+ UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
+ UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
+ UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
+ UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
+ UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
+ UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
}
// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody
@@ -34,6 +35,13 @@ func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCo
}
}
+// UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithIdentifierFirstMethod wrapped in UpdateLoginFlowBody
+func UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithIdentifierFirstMethod) UpdateLoginFlowBody {
+ return UpdateLoginFlowBody{
+ UpdateLoginFlowWithIdentifierFirstMethod: v,
+ }
+}
+
// UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody
func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody {
return UpdateLoginFlowBody{
@@ -98,6 +106,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'identifier_first'
+ if jsonDict["method"] == "identifier_first" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'lookup_secret'
if jsonDict["method"] == "lookup_secret" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -182,6 +202,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateLoginFlowWithIdentifierFirstMethod'
+ if jsonDict["method"] == "updateLoginFlowWithIdentifierFirstMethod" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod'
if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -263,6 +295,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateLoginFlowWithCodeMethod)
}
+ if src.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod)
+ }
+
if src.UpdateLoginFlowWithLookupSecretMethod != nil {
return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod)
}
@@ -299,6 +335,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} {
return obj.UpdateLoginFlowWithCodeMethod
}
+ if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return obj.UpdateLoginFlowWithIdentifierFirstMethod
+ }
+
if obj.UpdateLoginFlowWithLookupSecretMethod != nil {
return obj.UpdateLoginFlowWithLookupSecretMethod
}
diff --git a/internal/httpclient/model_update_login_flow_with_identifier_first_method.go b/internal/httpclient/model_update_login_flow_with_identifier_first_method.go
new file mode 100644
index 000000000000..70cf8002990d
--- /dev/null
+++ b/internal/httpclient/model_update_login_flow_with_identifier_first_method.go
@@ -0,0 +1,212 @@
+/*
+ * Ory Identities API
+ *
+ * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more.
+ *
+ * API version:
+ * Contact: office@ory.sh
+ */
+
+// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
+
+package client
+
+import (
+ "encoding/json"
+)
+
+// UpdateLoginFlowWithIdentifierFirstMethod Update Login Flow with Multi-Step Method
+type UpdateLoginFlowWithIdentifierFirstMethod struct {
+ // Sending the anti-csrf token is only required for browser login flows.
+ CsrfToken *string `json:"csrf_token,omitempty"`
+ // Identifier is the email or username of the user trying to log in.
+ Identifier string `json:"identifier"`
+ // Method should be set to \"password\" when logging in using the identifier and password strategy.
+ Method string `json:"method"`
+ // Transient data to pass along to any webhooks
+ TransientPayload map[string]interface{} `json:"transient_payload,omitempty"`
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethod instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewUpdateLoginFlowWithIdentifierFirstMethod(identifier string, method string) *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ this.Identifier = identifier
+ this.Method = method
+ return &this
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults() *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ return &this
+}
+
+// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfToken() string {
+ if o == nil || o.CsrfToken == nil {
+ var ret string
+ return ret
+ }
+ return *o.CsrfToken
+}
+
+// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfTokenOk() (*string, bool) {
+ if o == nil || o.CsrfToken == nil {
+ return nil, false
+ }
+ return o.CsrfToken, true
+}
+
+// HasCsrfToken returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasCsrfToken() bool {
+ if o != nil && o.CsrfToken != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetCsrfToken(v string) {
+ o.CsrfToken = &v
+}
+
+// GetIdentifier returns the Identifier field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifier() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Identifier
+}
+
+// GetIdentifierOk returns a tuple with the Identifier field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifierOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Identifier, true
+}
+
+// SetIdentifier sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetIdentifier(v string) {
+ o.Identifier = v
+}
+
+// GetMethod returns the Method field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethod() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Method
+}
+
+// GetMethodOk returns a tuple with the Method field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethodOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Method, true
+}
+
+// SetMethod sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetMethod(v string) {
+ o.Method = v
+}
+
+// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayload() map[string]interface{} {
+ if o == nil || o.TransientPayload == nil {
+ var ret map[string]interface{}
+ return ret
+ }
+ return o.TransientPayload
+}
+
+// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayloadOk() (map[string]interface{}, bool) {
+ if o == nil || o.TransientPayload == nil {
+ return nil, false
+ }
+ return o.TransientPayload, true
+}
+
+// HasTransientPayload returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool {
+ if o != nil && o.TransientPayload != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetTransientPayload(v map[string]interface{}) {
+ o.TransientPayload = v
+}
+
+func (o UpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if o.CsrfToken != nil {
+ toSerialize["csrf_token"] = o.CsrfToken
+ }
+ if true {
+ toSerialize["identifier"] = o.Identifier
+ }
+ if true {
+ toSerialize["method"] = o.Method
+ }
+ if o.TransientPayload != nil {
+ toSerialize["transient_payload"] = o.TransientPayload
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableUpdateLoginFlowWithIdentifierFirstMethod struct {
+ value *UpdateLoginFlowWithIdentifierFirstMethod
+ isSet bool
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) Get() *UpdateLoginFlowWithIdentifierFirstMethod {
+ return v.value
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Set(val *UpdateLoginFlowWithIdentifierFirstMethod) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableUpdateLoginFlowWithIdentifierFirstMethod(val *UpdateLoginFlowWithIdentifierFirstMethod) *NullableUpdateLoginFlowWithIdentifierFirstMethod {
+ return &NullableUpdateLoginFlowWithIdentifierFirstMethod{value: val, isSet: true}
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go
index 64374c620f8f..82a578cfc4d3 100644
--- a/internal/httpclient/model_update_registration_flow_body.go
+++ b/internal/httpclient/model_update_registration_flow_body.go
@@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct {
UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod
UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod
UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod
+ UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod
UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod
}
@@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd
}
}
+// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody
+func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody {
+ return UpdateRegistrationFlowBody{
+ UpdateRegistrationFlowWithProfileMethod: v,
+ }
+}
+
// UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody
func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody {
return UpdateRegistrationFlowBody{
@@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
- // check if the discriminator value is 'passKey'
- if jsonDict["method"] == "passKey" {
+ // check if the discriminator value is 'passkey'
+ if jsonDict["method"] == "passkey" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod
err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod)
if err == nil {
@@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'profile'
+ if jsonDict["method"] == "profile" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'webauthn'
if jsonDict["method"] == "webauthn" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod'
+ if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod'
if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod)
}
+ if src.UpdateRegistrationFlowWithProfileMethod != nil {
+ return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod)
+ }
+
if src.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod)
}
@@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} {
return obj.UpdateRegistrationFlowWithPasswordMethod
}
+ if obj.UpdateRegistrationFlowWithProfileMethod != nil {
+ return obj.UpdateRegistrationFlowWithProfileMethod
+ }
+
if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return obj.UpdateRegistrationFlowWithWebAuthnMethod
}
diff --git a/internal/registrationhelpers/helpers.go b/internal/registrationhelpers/helpers.go
index 9fbf7f08211d..6fd76bd6ef1a 100644
--- a/internal/registrationhelpers/helpers.go
+++ b/internal/registrationhelpers/helpers.go
@@ -288,7 +288,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
t.Run("description=can call endpoints only without session", func(t *testing.T) {
values := url.Values{}
t.Run("type=browser", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded"))
require.NoError(t, err)
defer res.Body.Close()
@@ -297,7 +297,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
})
t.Run("type=api", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json"))
require.NoError(t, err)
assert.Len(t, res.Cookies(), 0)
@@ -337,7 +337,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
values := url.Values{}
t.Run("type=browser", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded"))
require.NoError(t, err)
defer res.Body.Close()
@@ -346,7 +346,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
})
t.Run("type=api", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json"))
require.NoError(t, err)
assert.Len(t, res.Cookies(), 0)
diff --git a/internal/testhelpers/selfservice_login.go b/internal/testhelpers/selfservice_login.go
index 2bd20f81dfd4..1ef8db2e58ba 100644
--- a/internal/testhelpers/selfservice_login.go
+++ b/internal/testhelpers/selfservice_login.go
@@ -59,6 +59,18 @@ type initFlowOptions struct {
refresh bool
oauth2LoginChallenge string
via string
+ ctx context.Context
+}
+
+func newInitFlowOptions(opts []InitFlowWithOption) *initFlowOptions {
+ return new(initFlowOptions).apply(opts)
+}
+
+func (o *initFlowOptions) Context() context.Context {
+ if o.ctx == nil {
+ return context.Background()
+ }
+ return o.ctx
}
func (o *initFlowOptions) apply(opts []InitFlowWithOption) *initFlowOptions {
@@ -116,6 +128,12 @@ func InitFlowWithRefresh() InitFlowWithOption {
}
}
+func InitFlowWithContext(ctx context.Context) InitFlowWithOption {
+ return func(o *initFlowOptions) {
+ o.ctx = ctx
+ }
+}
+
func InitFlowWithOAuth2LoginChallenge(hlc string) InitFlowWithOption {
return func(o *initFlowOptions) {
o.oauth2LoginChallenge = hlc
@@ -134,12 +152,13 @@ func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httpte
req, err := http.NewRequest("GET", getURLFromInitOptions(ts, login.RouteInitBrowserFlow, forced, opts...), nil)
require.NoError(t, err)
+ o := newInitFlowOptions(opts)
if isSPA {
req.Header.Set("Accept", "application/json")
}
- res, err := client.Do(req)
+ res, err := client.Do(req.WithContext(o.Context()))
require.NoError(t, err)
body := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
@@ -167,11 +186,11 @@ func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httpte
return rs
}
-func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
+func InitializeLoginFlowViaAPIWithContext(t *testing.T, ctx context.Context, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
publicClient := NewSDKCustomClient(ts, client)
o := new(initFlowOptions).apply(opts)
- req := publicClient.FrontendApi.CreateNativeLoginFlow(context.Background()).Refresh(forced)
+ req := publicClient.FrontendApi.CreateNativeLoginFlow(ctx).Refresh(forced)
if o.aal != "" {
req = req.Aal(string(o.aal))
}
@@ -186,6 +205,10 @@ func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.S
return rs
}
+func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
+ return InitializeLoginFlowViaAPIWithContext(t, context.Background(), client, ts, forced, opts...)
+}
+
func LoginMakeRequest(
t *testing.T,
isAPI bool,
@@ -193,6 +216,18 @@ func LoginMakeRequest(
f *kratos.LoginFlow,
hc *http.Client,
values string,
+) (string, *http.Response) {
+ return LoginMakeRequestWithContext(t, context.Background(), isAPI, isSPA, f, hc, values)
+}
+
+func LoginMakeRequestWithContext(
+ t *testing.T,
+ ctx context.Context,
+ isAPI bool,
+ isSPA bool,
+ f *kratos.LoginFlow,
+ hc *http.Client,
+ values string,
) (string, *http.Response) {
require.NotEmpty(t, f.Ui.Action)
@@ -201,7 +236,7 @@ func LoginMakeRequest(
req.Header.Set("Accept", "application/json")
}
- res, err := hc.Do(req)
+ res, err := hc.Do(req.WithContext(ctx))
require.NoError(t, err, "action: %s", f.Ui.Action)
defer res.Body.Close()
diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go
index 29e8997f3438..1d2cecc824db 100644
--- a/internal/testhelpers/session.go
+++ b/internal/testhelpers/session.go
@@ -42,25 +42,25 @@ func NewSessionClient(t *testing.T, u string) *http.Client {
return c
}
-func maybePersistSession(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) {
- id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), sess.Identity.ID)
+func maybePersistSession(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) {
+ id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID)
if err != nil {
- require.NoError(t, sess.Identity.SetAvailableAAL(context.Background(), reg.IdentityManager()))
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), sess.Identity))
- id, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), sess.Identity.ID)
+ require.NoError(t, sess.Identity.SetAvailableAAL(ctx, reg.IdentityManager()))
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, sess.Identity))
+ id, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID)
require.NoError(t, err)
}
sess.Identity = id
sess.IdentityID = id.ID
- require.NoError(t, err, reg.SessionPersister().UpsertSession(context.Background(), sess))
+ require.NoError(t, err, reg.SessionPersister().UpsertSession(ctx, sess))
}
-func NewHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewHTTPClientWithSessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess))
+ require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
})
if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok {
@@ -75,11 +75,11 @@ func NewHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, s
return c
}
-func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess))
+ require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
})
if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok {
@@ -96,11 +96,11 @@ func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, reg *driver.RegistryD
return c
}
-func NewNoRedirectHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewNoRedirectHTTPClientWithSessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess))
+ require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
}))
defer ts.Close()
@@ -130,8 +130,8 @@ func (ct *TransportWithLogger) RoundTrip(req *http.Request) (*http.Response, err
return ct.RoundTripper.RoundTrip(req)
}
-func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewHTTPClientWithSessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
return &http.Client{
Transport: NewTransportWithHeader(t, http.Header{
@@ -140,10 +140,14 @@ func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, se
}
}
-func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDefault) *http.Client {
+func NewHTTPClientWithArbitrarySessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client {
+ return NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, nil)
+}
+
+func NewHTTPClientWithArbitrarySessionTokenAndTraits(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, traits identity.Traits) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
- &identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
+ &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: traits},
NewSessionLifespanProvider(time.Hour),
time.Now(),
identity.CredentialsTypePassword,
@@ -151,10 +155,10 @@ func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDe
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionToken(t, reg, s)
+ return NewHTTPClientWithSessionToken(t, ctx, reg, s)
}
-func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client {
+func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: []byte("{}")},
@@ -165,10 +169,10 @@ func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryD
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionCookie(t, reg, s)
+ return NewHTTPClientWithSessionCookie(t, ctx, reg, s)
}
-func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client {
+func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
@@ -179,10 +183,10 @@ func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewNoRedirectHTTPClientWithSessionCookie(t, reg, s)
+ return NewNoRedirectHTTPClientWithSessionCookie(t, ctx, reg, s)
}
-func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
+func NewHTTPClientWithIdentitySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
id,
@@ -193,10 +197,10 @@ func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDe
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionCookie(t, reg, s)
+ return NewHTTPClientWithSessionCookie(t, ctx, reg, s)
}
-func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
+func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
id,
@@ -207,10 +211,10 @@ func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, reg *driver.R
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionCookieLocalhost(t, reg, s)
+ return NewHTTPClientWithSessionCookieLocalhost(t, ctx, reg, s)
}
-func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
+func NewHTTPClientWithIdentitySessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
id,
@@ -221,7 +225,7 @@ func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDef
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionToken(t, reg, s)
+ return NewHTTPClientWithSessionToken(t, ctx, reg, s)
}
func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, methods ...string) {
@@ -236,8 +240,8 @@ func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, me
assert.Len(t, gjson.GetBytes(sess, "authentication_methods").Array(), 1+len(methods))
}
-func NewAuthorizedTransport(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader {
- maybePersistSession(t, reg, sess)
+func NewAuthorizedTransport(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader {
+ maybePersistSession(t, ctx, reg, sess)
return NewTransportWithHeader(t, http.Header{
"Authorization": {"Bearer " + sess.Token},
@@ -259,6 +263,10 @@ type TransportWithHeader struct {
h http.Header
}
+func (ct *TransportWithHeader) GetHeader() http.Header {
+ return ct.h
+}
+
func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) {
for k := range ct.h {
req.Header.Set(k, ct.h.Get(k))
diff --git a/schema/errors.go b/schema/errors.go
index 30c1f72976f3..6ff52a5047c2 100644
--- a/schema/errors.go
+++ b/schema/errors.go
@@ -117,12 +117,21 @@ func NewInvalidCredentialsError() error {
ValidationError: &jsonschema.ValidationError{
Message: `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`,
InstancePtr: "#/",
- Context: &ValidationErrorContextPasswordPolicyViolation{},
},
Messages: new(text.Messages).Add(text.NewErrorValidationInvalidCredentials()),
})
}
+func NewAccountNotFoundError() error {
+ return errors.WithStack(&ValidationError{
+ ValidationError: &jsonschema.ValidationError{
+ Message: "this account does not exist or has no login method configured",
+ InstancePtr: "#/identifier",
+ },
+ Messages: new(text.Messages).Add(text.NewErrorValidationAccountNotFound()),
+ })
+}
+
type ValidationErrorContextDuplicateCredentialsError struct {
AvailableCredentials []string `json:"available_credential_types"`
AvailableOIDCProviders []string `json:"available_oidc_providers"`
diff --git a/schema/handler_test.go b/schema/handler_test.go
index a5bdf06a5a83..eca595c7e5d4 100644
--- a/schema/handler_test.go
+++ b/schema/handler_test.go
@@ -15,7 +15,7 @@ import (
"strings"
"testing"
- "github.com/ory/client-go"
+ client "github.com/ory/kratos/internal/httpclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go
index 7a5f9ce22410..9bc9e6152d78 100644
--- a/selfservice/flow/continue_with.go
+++ b/selfservice/flow/continue_with.go
@@ -89,6 +89,8 @@ type ContinueWithVerificationUIFlow struct {
// The URL of the verification flow
//
+ // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ //
// required: false
URL string `json:"url,omitempty"`
}
@@ -134,8 +136,11 @@ type ContinueWithSettingsUI struct {
//
// required: true
Action ContinueWithActionShowSettingsUI `json:"action"`
+
// Flow contains the ID of the verification flow
//
+ // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ //
// required: true
Flow ContinueWithSettingsUIFlow `json:"flow"`
}
@@ -146,13 +151,21 @@ type ContinueWithSettingsUIFlow struct {
//
// required: true
ID uuid.UUID `json:"id"`
+
+ // The URL of the settings flow
+ //
+ // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ //
+ // required: false
+ URL string `json:"url,omitempty"`
}
-func NewContinueWithSettingsUI(f Flow) *ContinueWithSettingsUI {
+func NewContinueWithSettingsUI(f Flow, redirectTo string) *ContinueWithSettingsUI {
return &ContinueWithSettingsUI{
Action: ContinueWithActionShowSettingsUIString,
Flow: ContinueWithSettingsUIFlow{
- ID: f.GetID(),
+ ID: f.GetID(),
+ URL: redirectTo,
},
}
}
@@ -188,6 +201,8 @@ type ContinueWithRecoveryUIFlow struct {
// The URL of the recovery flow
//
+ // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ //
// required: false
URL string `json:"url,omitempty"`
}
@@ -201,6 +216,36 @@ func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI {
}
}
+// swagger:enum ContinueWithActionRedirectBrowserTo
+type ContinueWithActionRedirectBrowserTo string
+
+// #nosec G101 -- only a key constant
+const (
+ ContinueWithActionRedirectBrowserToString ContinueWithActionRedirectBrowserTo = "redirect_browser_to"
+)
+
+// Indicates, that the UI flow could be continued by showing a recovery ui
+//
+// swagger:model continueWithRedirectBrowserTo
+type ContinueWithRedirectBrowserTo struct {
+ // Action will always be `redirect_browser_to`
+ //
+ // required: true
+ Action ContinueWithActionRedirectBrowserTo `json:"action"`
+
+ // The URL to redirect the browser to
+ //
+ // required: true
+ RedirectTo string `json:"redirect_browser_to"`
+}
+
+func NewContinueWithRedirectBrowserTo(redirectTo string) *ContinueWithRedirectBrowserTo {
+ return &ContinueWithRedirectBrowserTo{
+ Action: ContinueWithActionRedirectBrowserToString,
+ RedirectTo: redirectTo,
+ }
+}
+
func ErrorWithContinueWith(err *herodot.DefaultError, continueWith ...ContinueWith) *herodot.DefaultError {
if err.DetailsField == nil {
err.DetailsField = map[string]interface{}{}
diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go
index 5cc78c35bda1..cebebc45bd0e 100644
--- a/selfservice/flow/login/error_test.go
+++ b/selfservice/flow/login/error_test.go
@@ -74,7 +74,12 @@ func TestHandleError(t *testing.T) {
require.NoError(t, err)
for _, s := range reg.LoginStrategies(context.Background()) {
- require.NoError(t, s.PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f))
+ switch s.(type) {
+ case login.OneStepFormHydrator:
+ require.NoError(t, s.(login.OneStepFormHydrator).PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f))
+ case login.FormHydrator:
+ require.NoError(t, s.(login.FormHydrator).PopulateLoginMethodFirstFactor(req, f))
+ }
}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), f))
diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go
index a01d449a2751..06b189d6c351 100644
--- a/selfservice/flow/login/flow.go
+++ b/selfservice/flow/login/flow.go
@@ -230,9 +230,9 @@ func (f Flow) GetID() uuid.UUID {
return f.ID
}
-// IsForced returns true if the login flow was triggered to re-authenticate the user.
+// IsRefresh returns true if the login flow was triggered to re-authenticate the user.
// This is the case if the refresh query parameter is set to true.
-func (f *Flow) IsForced() bool {
+func (f *Flow) IsRefresh() bool {
return f.Refresh
}
diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go
index 88b3712602a0..51886511bf55 100644
--- a/selfservice/flow/login/handler.go
+++ b/selfservice/flow/login/handler.go
@@ -212,8 +212,39 @@ preLoginHook:
}
for _, s := range h.d.LoginStrategies(r.Context(), strategyFilters...) {
- if err := s.PopulateLoginMethod(r, f.RequestedAAL, f); err != nil {
- return nil, nil, err
+ var populateErr error
+
+ switch strategy := s.(type) {
+ case FormHydrator:
+ switch {
+ case f.RequestedAAL == identity.AuthenticatorAssuranceLevel1:
+ switch {
+ case f.IsRefresh():
+ // Refreshing takes precedence over identifier_first auth which can not be a refresh flow.
+ // Therefor this comes first.
+ populateErr = strategy.PopulateLoginMethodFirstFactorRefresh(r, f)
+ case h.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(r.Context()):
+ populateErr = strategy.PopulateLoginMethodIdentifierFirstIdentification(r, f)
+ default:
+ populateErr = strategy.PopulateLoginMethodFirstFactor(r, f)
+ }
+ case f.RequestedAAL == identity.AuthenticatorAssuranceLevel2:
+ switch {
+ case f.IsRefresh():
+ // Refresh takes precedence.
+ populateErr = strategy.PopulateLoginMethodSecondFactorRefresh(r, f)
+ default:
+ populateErr = strategy.PopulateLoginMethodSecondFactor(r, f)
+ }
+ }
+ case OneStepFormHydrator:
+ populateErr = strategy.PopulateLoginMethod(r, f.RequestedAAL, f)
+ default:
+ populateErr = errors.WithStack(x.PseudoPanic.WithReasonf("A login strategy was expected to implement one of the interfaces OneStepFormHydrator or FormHydrator but did not."))
+ }
+
+ if populateErr != nil {
+ return nil, nil, populateErr
}
}
diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go
index f0e06ccfc934..f662290d598b 100644
--- a/selfservice/flow/login/hook.go
+++ b/selfservice/flow/login/hook.go
@@ -159,6 +159,10 @@ func (e *HookExecutor) PostLoginHook(
"redirect_reason": "login successful",
})...)
+ if f.Type == flow.TypeBrowser {
+ f.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
+ }
+
classified := s
s = s.Declassified()
diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go
index fe73f22d7eef..7d7f8e158174 100644
--- a/selfservice/flow/login/hook_test.go
+++ b/selfservice/flow/login/hook_test.go
@@ -93,6 +93,17 @@ func TestLoginExecutor(t *testing.T) {
assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String())
})
+ t.Run("case=pass without hooks if client is ajax", func(t *testing.T) {
+ t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
+
+ ts := newServer(t, flow.TypeBrowser, nil)
+ res, body := makeRequestPost(t, ts, true, url.Values{})
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.Contains(t, res.Request.URL.String(), ts.URL)
+ assert.EqualValues(t, gjson.Get(body, "continue_with").Raw, `[{"action":"redirect_browser_to","redirect_browser_to":"https://www.ory.sh/"}]`)
+ t.Logf("%s", body)
+ })
+
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
viperSetPost(t, conf, strategy.String(), []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})
@@ -286,6 +297,7 @@ func TestLoginExecutor(t *testing.T) {
})
})
})
+
t.Run("case=maybe links credential", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go
index c70ad9cc8684..fec71d3beb1d 100644
--- a/selfservice/flow/login/strategy.go
+++ b/selfservice/flow/login/strategy.go
@@ -20,7 +20,6 @@ type Strategy interface {
ID() identity.CredentialsType
NodeGroup() node.UiNodeGroup
RegisterLoginRoutes(*x.RouterPublic)
- PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error
Login(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (i *identity.Identity, err error)
CompletedAuthenticationMethod(ctx context.Context, methods session.AuthenticationMethods) session.AuthenticationMethod
}
diff --git a/selfservice/flow/login/strategy_form_hydrator.go b/selfservice/flow/login/strategy_form_hydrator.go
new file mode 100644
index 000000000000..8f665a0f043f
--- /dev/null
+++ b/selfservice/flow/login/strategy_form_hydrator.go
@@ -0,0 +1,60 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package login
+
+import (
+ "net/http"
+
+ "github.com/pkg/errors"
+
+ "github.com/ory/kratos/identity"
+)
+
+type OneStepFormHydrator interface {
+ PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error
+}
+
+type FormHydrator interface {
+ PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *Flow) error
+ PopulateLoginMethodFirstFactor(r *http.Request, sr *Flow) error
+ PopulateLoginMethodSecondFactor(r *http.Request, sr *Flow) error
+ PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *Flow) error
+
+ // PopulateLoginMethodIdentifierFirstCredentials populates the login form with the first factor credentials.
+ // This method is called when the login flow is set to identifier first. The method will receive information
+ // about the identity that is being used to log in and the identifier that was used to find the identity.
+ //
+ // The method should populate the login form with the credentials of the identity.
+ //
+ // If the method can not find any credentials (because the identity does not exist) idfirst.ErrNoCredentialsFound
+ // must be returned. When returning idfirst.ErrNoCredentialsFound the strategy will appropriately deal with
+ // account enumeration mitigation.
+ //
+ // This method does however need to take appropriate steps to show/hide certain fields depending on the account
+ // enumeration configuration.
+ PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *Flow, options ...FormHydratorModifier) error
+ PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *Flow) error
+}
+
+var ErrBreakLoginPopulate = errors.New("skip rest of login form population")
+
+type FormHydratorOptions struct {
+ IdentityHint *identity.Identity
+}
+
+type FormHydratorModifier func(o *FormHydratorOptions)
+
+func WithIdentityHint(i *identity.Identity) FormHydratorModifier {
+ return func(o *FormHydratorOptions) {
+ o.IdentityHint = i
+ }
+}
+
+func NewFormHydratorOptions(modifiers []FormHydratorModifier) *FormHydratorOptions {
+ o := new(FormHydratorOptions)
+ for _, m := range modifiers {
+ m(o)
+ }
+ return o
+}
diff --git a/selfservice/flow/login/strategy_form_hydrator_test.go b/selfservice/flow/login/strategy_form_hydrator_test.go
new file mode 100644
index 000000000000..64d0f0f54d6d
--- /dev/null
+++ b/selfservice/flow/login/strategy_form_hydrator_test.go
@@ -0,0 +1,18 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package login
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/ory/kratos/identity"
+)
+
+func TestWithIdentityHint(t *testing.T) {
+ expected := new(identity.Identity)
+ opts := NewFormHydratorOptions([]FormHydratorModifier{WithIdentityHint(expected)})
+ assert.Equal(t, expected, opts.IdentityHint)
+}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json
index f4c0270da2dc..17eb6e965bcb 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json
index 56782eed4571..a9ad1e527fb4 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json
index f4c0270da2dc..17eb6e965bcb 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json
index 56782eed4571..a9ad1e527fb4 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go
index 6a997009c1c5..e44be2487bbb 100644
--- a/selfservice/flow/registration/hook.go
+++ b/selfservice/flow/registration/hook.go
@@ -192,12 +192,17 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque
if err != nil {
return err
}
+
span.SetAttributes(otelx.StringAttrs(map[string]string{
"return_to": returnTo.String(),
- "flow_type": string(flow.TypeBrowser),
+ "flow_type": string(registrationFlow.Type),
"redirect_reason": "registration successful",
})...)
+ if registrationFlow.Type == flow.TypeBrowser && x.IsJSONRequest(r) {
+ registrationFlow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
+ }
+
e.d.Audit().
WithRequest(r).
WithField("identity_id", i.ID).
diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go
index 3761692e3f45..9e60b33f1f52 100644
--- a/selfservice/flow/registration/hook_test.go
+++ b/selfservice/flow/registration/hook_test.go
@@ -91,6 +91,21 @@ func TestRegistrationExecutor(t *testing.T) {
assert.Equal(t, actual.Traits, i.Traits)
})
+ t.Run("case=pass without hooks if ajax client", func(t *testing.T) {
+ t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
+ i := testhelpers.SelfServiceHookFakeIdentity(t)
+
+ ts := newServer(t, i, flow.TypeBrowser)
+ res, body := makeRequestPost(t, ts, true, url.Values{})
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.Contains(t, res.Request.URL.String(), ts.URL)
+ assert.EqualValues(t, gjson.Get(body, "continue_with").Raw, `[{"action":"redirect_browser_to","redirect_browser_to":"https://www.ory.sh/"}]`)
+
+ actual, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
+ require.NoError(t, err)
+ assert.Equal(t, actual.Traits, i.Traits)
+ })
+
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
viperSetPost(t, conf, strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})
diff --git a/selfservice/flow/settings/error_test.go b/selfservice/flow/settings/error_test.go
index 5776cd2b6942..5cdebc400df6 100644
--- a/selfservice/flow/settings/error_test.go
+++ b/selfservice/flow/settings/error_test.go
@@ -153,7 +153,7 @@ func TestHandleError(t *testing.T) {
// This needs an authenticated client in order to call the RouteGetFlow endpoint
s, err := session.NewActiveSession(req, &id, testhelpers.NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
require.NoError(t, err)
- c := testhelpers.NewHTTPClientWithSessionToken(t, reg, s)
+ c := testhelpers.NewHTTPClientWithSessionToken(t, ctx, reg, s)
settingsFlow = newFlow(t, time.Minute, tc.t)
flowError = flow.NewFlowExpiredError(expiredAnHourAgo)
diff --git a/selfservice/flow/settings/handler_test.go b/selfservice/flow/settings/handler_test.go
index 35d34fd735fc..584a7f3e5535 100644
--- a/selfservice/flow/settings/handler_test.go
+++ b/selfservice/flow/settings/handler_test.go
@@ -67,8 +67,8 @@ func TestHandler(t *testing.T) {
primaryIdentity := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`)}
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), primaryIdentity))
- primaryUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, primaryIdentity)
- otherUser := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ primaryUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, primaryIdentity)
+ otherUser := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
newExpiredFlow := func() *settings.Flow {
f, err := settings.NewFlow(conf, -time.Minute,
@@ -133,7 +133,7 @@ func TestHandler(t *testing.T) {
return initAuthenticatedFlow(t, hc, false, true, opts...)
}
- aal2Identity := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{
+ aal2Identity := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{
State: identity.StateActive,
Traits: []byte(`{"email":"foo@bar"}`),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -151,7 +151,7 @@ func TestHandler(t *testing.T) {
})
t.Run("description=success", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
res, body := initFlow(t, user1, true)
assert.Contains(t, res.Request.URL.String(), settings.RouteInitAPIFlow)
assertion(t, body, true)
@@ -206,7 +206,7 @@ func TestHandler(t *testing.T) {
})
t.Run("description=success", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
res, body := initFlow(t, user1, false)
assert.Contains(t, res.Request.URL.String(), reg.Config().SelfServiceFlowSettingsUI(ctx).String())
assertion(t, body, false)
@@ -241,7 +241,7 @@ func TestHandler(t *testing.T) {
})
t.Run("case=redirects with 303", func(t *testing.T) {
- c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
// prevent the redirect
c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
@@ -268,7 +268,7 @@ func TestHandler(t *testing.T) {
})
t.Run("description=success", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
res, body := initSPAFlow(t, user1)
assert.Contains(t, res.Request.URL.String(), settings.RouteInitBrowserFlow)
assertion(t, body, false)
@@ -277,7 +277,7 @@ func TestHandler(t *testing.T) {
t.Run("description=can not init if identity has aal2 but session has aal1", func(t *testing.T) {
email := testhelpers.RandomEmail()
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL)
- user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{
+ user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{
State: identity.StateActive,
Traits: []byte(`{"email":"` + email + `"}`),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -303,7 +303,7 @@ func TestHandler(t *testing.T) {
t.Run("description=settings return_to should persist through mfa flows", func(t *testing.T) {
email := testhelpers.RandomEmail()
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL)
- user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{
+ user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{
State: identity.StateActive,
Traits: []byte(`{"email":"` + email + `"}`),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -356,7 +356,7 @@ func TestHandler(t *testing.T) {
returnTo := "https://www.ory.sh"
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo})
- client := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ client := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
body := testhelpers.EasyGetBody(t, client, publicTS.URL+settings.RouteInitBrowserFlow+"?return_to="+returnTo)
// Expire the flow
@@ -385,8 +385,8 @@ func TestHandler(t *testing.T) {
t.Run("description=should fail to fetch request if identity changed", func(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
res, err := user1.Get(publicTS.URL + settings.RouteInitAPIFlow)
require.NoError(t, err)
@@ -537,8 +537,8 @@ func TestHandler(t *testing.T) {
t.Run("description=fail to submit form as another user", func(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
_, body := initFlow(t, user1, true)
var f kratos.SettingsFlow
require.NoError(t, json.Unmarshal(body, &f))
@@ -549,8 +549,8 @@ func TestHandler(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
_, body := initFlow(t, user1, true)
var f kratos.SettingsFlow
require.NoError(t, json.Unmarshal(body, &f))
@@ -561,8 +561,8 @@ func TestHandler(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
_, body := initFlow(t, user1, true)
var f kratos.SettingsFlow
require.NoError(t, json.Unmarshal(body, &f))
@@ -602,7 +602,7 @@ func TestHandler(t *testing.T) {
t.Run("case=relative redirect when self-service settings ui is a relative url", func(t *testing.T) {
reg.Config().MustSet(ctx, config.ViperKeySelfServiceSettingsURL, "/settings-ts")
- user1 := testhelpers.NewNoRedirectHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewNoRedirectHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
res, _ := initFlow(t, user1, false)
assert.Regexp(
t,
diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go
index b688fd0fc431..88741e766736 100644
--- a/selfservice/flow/settings/hook.go
+++ b/selfservice/flow/settings/hook.go
@@ -308,6 +308,7 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request,
}
// ContinueWith items are transient items, not stored in the database, and need to be carried over here, so
// they can be returned to the client.
+ ctxUpdate.Flow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems
e.d.Writer().Write(w, r, updatedFlow)
diff --git a/selfservice/flow/settings/hook_test.go b/selfservice/flow/settings/hook_test.go
index 0ab28a1504f9..5253bb2886f2 100644
--- a/selfservice/flow/settings/hook_test.go
+++ b/selfservice/flow/settings/hook_test.go
@@ -101,6 +101,16 @@ func TestSettingsExecutor(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), uiURL)
})
+ t.Run("case=pass without hooks if ajax client", func(t *testing.T) {
+ t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
+
+ ts := newServer(t, nil, flow.TypeBrowser)
+ res, body := makeRequestPost(t, ts, true, url.Values{})
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.Contains(t, res.Request.URL.String(), ts.URL)
+ assert.EqualValues(t, gjson.Get(body, "continue_with.0.action").String(), "redirect_browser_to")
+ })
+
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go
index 76a0683fc19d..a6b4f1a98966 100644
--- a/selfservice/flow/state.go
+++ b/selfservice/flow/state.go
@@ -33,7 +33,11 @@ const (
StateSuccess State = "success"
)
-var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge}
+var states = []State{
+ StateChooseMethod,
+ StateEmailSent,
+ StatePassedChallenge,
+}
func indexOf(current State) int {
for k, s := range states {
diff --git a/selfservice/flowhelpers/login.go b/selfservice/flowhelpers/login.go
index 2e97f85ebe30..60c17176a740 100644
--- a/selfservice/flowhelpers/login.go
+++ b/selfservice/flowhelpers/login.go
@@ -15,11 +15,11 @@ func GuessForcedLoginIdentifier(r *http.Request, d interface {
session.ManagementProvider
identity.PrivilegedPoolProvider
}, f interface {
- IsForced() bool
+ IsRefresh() bool
}, ct identity.CredentialsType) (identifier string, id *identity.Identity, creds *identity.Credentials) {
var ok bool
// This block adds the identifier to the method when the request is forced - as a hint for the user.
- if !f.IsForced() {
+ if !f.IsRefresh() {
// do nothing
} else if sess, err := d.SessionManager().FetchFromRequest(r.Context(), r); err != nil {
// do nothing
diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json
index a9f46bedb5c4..736578d0e543 100644
--- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json
+++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json
@@ -6,7 +6,9 @@
"name": "code",
"type": "text",
"required": true,
+ "pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -31,8 +33,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json
new file mode 100644
index 000000000000..8e9874d00cbf
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json
new file mode 100644
index 000000000000..8e9874d00cbf
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..8e9874d00cbf
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_2fa_and_request_is_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_2fa_and_request_is_2fa.json
new file mode 100644
index 000000000000..60b142ed4181
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_2fa_and_request_is_2fa.json
@@ -0,0 +1,66 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [
+ {
+ "id": 1010020,
+ "text": "We will send a code to fo****@ory.sh. To verify that this is your address please enter it here.",
+ "type": "info",
+ "context": {
+ "masked_to": "fo****@ory.sh"
+ }
+ }
+ ],
+ "meta": {
+ "label": {
+ "id": 1070002,
+ "text": "",
+ "type": "info",
+ "context": {
+ "title": ""
+ }
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010019,
+ "text": "Continue with code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_passwordless_login_and_request_is_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_passwordless_login_and_request_is_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_passwordless_login_and_request_is_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..60b142ed4181
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa.json
@@ -0,0 +1,66 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [
+ {
+ "id": 1010020,
+ "text": "We will send a code to fo****@ory.sh. To verify that this is your address please enter it here.",
+ "type": "info",
+ "context": {
+ "masked_to": "fo****@ory.sh"
+ }
+ }
+ ],
+ "meta": {
+ "label": {
+ "id": 1070002,
+ "text": "",
+ "type": "info",
+ "context": {
+ "title": ""
+ }
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010019,
+ "text": "Continue with code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json
new file mode 100644
index 000000000000..60b142ed4181
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json
@@ -0,0 +1,66 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [
+ {
+ "id": 1010020,
+ "text": "We will send a code to fo****@ory.sh. To verify that this is your address please enter it here.",
+ "type": "info",
+ "context": {
+ "masked_to": "fo****@ory.sh"
+ }
+ }
+ ],
+ "meta": {
+ "label": {
+ "id": 1070002,
+ "text": "",
+ "type": "info",
+ "context": {
+ "title": ""
+ }
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010019,
+ "text": "Continue with code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
index 37f61ac9e827..01def57fd58f 100644
--- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
+++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
@@ -30,8 +30,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
index 42456da54dc5..fde2aae2986f 100644
--- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
+++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
@@ -19,7 +19,9 @@
"name": "code",
"type": "text",
"required": true,
+ "pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -44,8 +46,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go
index ee3ce353e4ae..351949f7cd95 100644
--- a/selfservice/strategy/code/strategy.go
+++ b/selfservice/strategy/code/strategy.go
@@ -180,6 +180,7 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error {
if f.GetType() == flow.TypeBrowser {
f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r))
}
+
return nil
}
@@ -192,7 +193,7 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error
node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).
WithMetaLabel(text.NewInfoNodeInputEmail()),
)
- codeMetaLabel = text.NewInfoNodeLabelSubmit()
+ codeMetaLabel = text.NewInfoNodeLabelContinue()
case *login.Flow:
ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(ctx)
if err != nil {
@@ -299,15 +300,10 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error
// preserve the login identifier that was submitted
// so we can retry the code flow with the same data
for _, n := range f.GetUI().Nodes {
- if n.Group == node.DefaultGroup {
- // we don't need the user to change the values here
- // for better UX let's make them disabled
- // when there are errors we won't hide the fields
- if len(n.Messages) == 0 {
- if input, ok := n.Attributes.(*node.InputAttributes); ok {
- input.Type = "hidden"
- n.Attributes = input
- }
+ if n.ID() == "identifier" {
+ if input, ok := n.Attributes.(*node.InputAttributes); ok {
+ input.Type = "hidden"
+ n.Attributes = input
}
freshNodes = append(freshNodes, n)
}
@@ -367,13 +363,16 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error
)
// code input field
- freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).
+ freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) {
+ a.Pattern = "[0-9]+"
+ a.MaxLength = CodeLength
+ })).
WithMetaLabel(codeMetaLabel))
// code submit button
freshNodes.
Append(node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
if resendNode != nil {
freshNodes.Append(resendNode)
diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go
index a9d7459f5c56..a6d9af4fad0f 100644
--- a/selfservice/strategy/code/strategy_login.go
+++ b/selfservice/strategy/code/strategy_login.go
@@ -10,6 +10,9 @@ import (
"net/http"
"strings"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+ "github.com/ory/kratos/text"
+
"github.com/ory/x/sqlcon"
"github.com/pkg/errors"
@@ -29,7 +32,10 @@ import (
"github.com/ory/x/decoderx"
)
-var _ login.Strategy = new(Strategy)
+var (
+ _ login.FormHydrator = new(Strategy)
+ _ login.Strategy = new(Strategy)
+)
// Update Login flow using the code method
//
@@ -112,10 +118,6 @@ func (s *Strategy) HandleLoginError(r *http.Request, f *login.Flow, body *update
return err
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, lf *login.Flow) error {
- return s.PopulateMethod(r, lf)
-}
-
// findIdentityByIdentifier returns the identity and the code credential for the given identifier.
// If the identity does not have a code credential, it will attempt to find
// the identity through other credentials matching the identifier.
@@ -372,3 +374,41 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi
return i, nil
}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, f *login.Flow, opts ...login.FormHydratorModifier) error {
+ if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled {
+ // We only return this if passwordless is disabled, because if it is enabled we can always sign in using this method.
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+ o := login.NewFormHydratorOptions(opts)
+
+ // If the identity hint is nil and account enumeration mitigation is disabled, we return an error.
+ if o.IdentityHint == nil && !s.deps.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ f.GetUI().Nodes.Append(
+ node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginCode()),
+ )
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error {
+ return nil
+}
diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go
index 19cac6d38375..660e6f3352f8 100644
--- a/selfservice/strategy/code/strategy_login_test.go
+++ b/selfservice/strategy/code/strategy_login_test.go
@@ -11,6 +11,16 @@ import (
"net/http/httptest"
"net/url"
"testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/ory/kratos/driver"
+ "github.com/ory/kratos/selfservice/flow/login"
+
+ "github.com/ory/kratos/selfservice/flow"
"github.com/ory/x/ioutilx"
"github.com/ory/x/snapshotx"
@@ -32,6 +42,41 @@ import (
"github.com/ory/x/sqlxx"
)
+func createIdentity(ctx context.Context, t *testing.T, reg driver.Registry, withoutCodeCredential bool, moreIdentifiers ...string) *identity.Identity {
+ t.Helper()
+ i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
+ email := testhelpers.RandomEmail()
+
+ ids := fmt.Sprintf(`"email":"%s"`, email)
+ for i, identifier := range moreIdentifiers {
+ ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier)
+ }
+
+ i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, %s}`, ids))
+
+ credentials := map[identity.CredentialsType]identity.Credentials{
+ identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
+ identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
+ identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")},
+ }
+ if !withoutCodeCredential {
+ credentials[identity.CredentialsTypeCodeAuth] = identity.Credentials{Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"address_type\": \"email\", \"used_at\": \"2023-07-26T16:59:06+02:00\"}")}
+ }
+ i.Credentials = credentials
+
+ var va []identity.VerifiableAddress
+ for _, identifier := range moreIdentifiers {
+ va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted})
+ }
+
+ va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted})
+
+ i.VerifiableAddresses = va
+
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
+ return i
+}
+
func TestLoginCodeStrategy(t *testing.T) {
ctx := context.Background()
conf, reg := internal.NewFastRegistryWithMocks(t)
@@ -46,41 +91,6 @@ func TestLoginCodeStrategy(t *testing.T) {
public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg)
- createIdentity := func(ctx context.Context, t *testing.T, withoutCodeCredential bool, moreIdentifiers ...string) *identity.Identity {
- t.Helper()
- i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
- email := testhelpers.RandomEmail()
-
- ids := fmt.Sprintf(`"email":"%s"`, email)
- for i, identifier := range moreIdentifiers {
- ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier)
- }
-
- i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, %s}`, ids))
-
- credentials := map[identity.CredentialsType]identity.Credentials{
- identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
- identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
- identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")},
- }
- if !withoutCodeCredential {
- credentials[identity.CredentialsTypeCodeAuth] = identity.Credentials{Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"address_type\": \"email\", \"used_at\": \"2023-07-26T16:59:06+02:00\"}")}
- }
- i.Credentials = credentials
-
- var va []identity.VerifiableAddress
- for _, identifier := range moreIdentifiers {
- va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted})
- }
-
- va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted})
-
- i.VerifiableAddresses = va
-
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
- return i
- }
-
type state struct {
flowID string
identity *identity.Identity
@@ -102,7 +112,7 @@ func TestLoginCodeStrategy(t *testing.T) {
createLoginFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, withoutCodeCredential bool, moreIdentifiers ...string) *state {
t.Helper()
- identity := createIdentity(ctx, t, withoutCodeCredential, moreIdentifiers...)
+ identity := createIdentity(ctx, t, reg, withoutCodeCredential, moreIdentifiers...)
var client *http.Client
if apiType == ApiTypeNative {
@@ -247,9 +257,15 @@ func TestLoginCodeStrategy(t *testing.T) {
assert.NotEmpty(t, loginCode)
// 3. Submit OTP
- submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) {
+ state := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) {
v.Set("code", loginCode)
}, true, nil)
+ if tc.apiType == ApiTypeSPA {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body)
+ assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body)
+ } else {
+ assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body)
+ }
})
t.Run("case=new identities automatically have login with code", func(t *testing.T) {
@@ -587,14 +603,14 @@ func TestLoginCodeStrategy(t *testing.T) {
t.Run("case=should be able to get AAL2 session", func(t *testing.T) {
t.Cleanup(testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json")) // doesn't have the code credential
- identity := createIdentity(ctx, t, true)
+ identity := createIdentity(ctx, t, reg, true)
var cl *http.Client
var f *oryClient.LoginFlow
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
}
@@ -627,14 +643,14 @@ func TestLoginCodeStrategy(t *testing.T) {
testhelpers.EnsureAAL(t, cl, public, "aal2", "code")
})
t.Run("case=cannot use different identifier", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var f *oryClient.LoginFlow
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
}
@@ -661,14 +677,14 @@ func TestLoginCodeStrategy(t *testing.T) {
t.Run("case=verify initial payload", func(t *testing.T) {
fixedEmail := fmt.Sprintf("fixed_mfa_test_%s@ory.sh", tc.apiType)
- identity := createIdentity(ctx, t, false, fixedEmail)
+ identity := createIdentity(ctx, t, reg, false, fixedEmail)
var cl *http.Client
var f *oryClient.LoginFlow
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"))
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"))
}
@@ -678,15 +694,15 @@ func TestLoginCodeStrategy(t *testing.T) {
})
t.Run("case=using a non existing identity trait results in an error", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var res *http.Response
var err error
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=doesnt_exist")
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=doesnt_exist")
}
require.NoError(t, err)
@@ -699,15 +715,15 @@ func TestLoginCodeStrategy(t *testing.T) {
})
t.Run("case=missing via parameter results results in an error", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var res *http.Response
var err error
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2")
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2")
}
require.NoError(t, err)
@@ -718,16 +734,17 @@ func TestLoginCodeStrategy(t *testing.T) {
}
require.Equal(t, "AAL2 login via code requires the `via` query parameter", gjson.GetBytes(body, "reason").String(), "%s", body)
})
+
t.Run("case=unset trait in identity should lead to an error", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var res *http.Response
var err error
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=email_1")
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=email_1")
}
require.NoError(t, err)
@@ -742,3 +759,236 @@ func TestLoginCodeStrategy(t *testing.T) {
})
}
}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{
+ "enabled": true,
+ "passwordless_enabled": true,
+ })
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/default.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeCodeAuth)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ passwordlessEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{
+ "enabled": true,
+ "passwordless_enabled": true,
+ "mfa_enabled": false,
+ })
+
+ mfaEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{
+ "enabled": true,
+ "passwordless_enabled": false,
+ "mfa_enabled": true,
+ })
+
+ toMFARequest := func(r *http.Request, f *login.Flow) {
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ r.URL = &url.URL{Path: "/", RawQuery: "via=email"}
+ // I only fear god.
+ r.Header = testhelpers.NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, []byte(`{"email":"foo@ory.sh"}`)).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ test := func(t *testing.T, ctx context.Context) {
+ r, f := newFlow(ctx, t)
+ toMFARequest(r, f)
+
+ r.Header = testhelpers.NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, []byte(`{"email":"foo@ory.sh"}`)).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+
+ // We still use the legacy hydrator under the hood here and thus need to set this correctly.
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ r.URL = &url.URL{Path: "/", RawQuery: "via=email"}
+
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ }
+
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ test(t, mfaEnabled)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ test(t, passwordlessEnabled)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ t.Run("case=code is used for 2fa but request is 1fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login and request is 1fa", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ t.Run("case=code is used for passwordless login and request is 1fa with refresh", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for 2fa and request is 1fa with refresh", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ t.Run("case=code is used for 2fa and request is 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ toMFARequest(r, f)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login and request is 2fa", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ toMFARequest(r, f)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ t.Run("case=code is used for 2fa and request is 2fa with refresh", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ toMFARequest(r, f)
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login and request is 2fa with refresh", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ toMFARequest(r, f)
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ t.Run("case=with no identity", func(t *testing.T) {
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ t.Run("case=identity has code method", func(t *testing.T) {
+ identifier := x.NewUUID().String()
+ id := createIdentity(ctx, t, reg, false, identifier)
+
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=identity does not have a code method", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go
index 758e81d04fd9..f33356f2df31 100644
--- a/selfservice/strategy/code/strategy_recovery.go
+++ b/selfservice/strategy/code/strategy_recovery.go
@@ -43,7 +43,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err
f.UI.
GetNodes().
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
return nil
}
@@ -235,12 +235,13 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request,
}
if s.deps.Config().UseContinueWithTransitions(ctx) {
+ redirectTo := sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String()
switch {
case f.Type.IsAPI(), x.IsJSONRequest(r):
- f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf))
+ f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf, redirectTo))
s.deps.Writer().Write(w, r, f)
default:
- http.Redirect(w, r, sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String(), http.StatusSeeOther)
+ http.Redirect(w, r, redirectTo, http.StatusSeeOther)
}
} else {
if x.IsJSONRequest(r) {
@@ -405,6 +406,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R
f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithInputAttributes(func(a *node.InputAttributes) {
a.Required = true
a.Pattern = "[0-9]+"
+ a.MaxLength = CodeLength
})).
WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()),
)
@@ -413,7 +415,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R
f.UI.
GetNodes().
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
f.UI.Nodes.Append(node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit).
WithMetaLabel(text.NewInfoNodeResendOTP()),
diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go
index 028bb811bcaa..63aa36a90edd 100644
--- a/selfservice/strategy/code/strategy_recovery_admin.go
+++ b/selfservice/strategy/code/strategy_recovery_admin.go
@@ -178,13 +178,16 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http.
recoveryFlow.DangerousSkipCSRFCheck = true
recoveryFlow.State = flow.StateEmailSent
recoveryFlow.UI.Nodes = node.Nodes{}
- recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).
+ recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) {
+ a.Pattern = "[0-9]+"
+ a.MaxLength = CodeLength
+ })).
WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()),
)
recoveryFlow.UI.Nodes.
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil {
s.deps.Writer().WriteError(w, r, err)
diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go
index 98f3d9c7f21c..c5016b28710b 100644
--- a/selfservice/strategy/code/strategy_recovery_test.go
+++ b/selfservice/strategy/code/strategy_recovery_test.go
@@ -532,7 +532,7 @@ func TestRecovery(t *testing.T) {
require.NoError(t, err)
// Add the authentication to the request
- client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, reg, session), t).RoundTripper
+ client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, ctx, reg, session), t).RoundTripper
v := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v.Set("email", "some-email@example.org")
@@ -1373,7 +1373,7 @@ func TestRecovery_WithContinueWith(t *testing.T) {
require.NoError(t, err)
// Add the authentication to the request
- client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, reg, session), t).RoundTripper
+ client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, ctx, reg, session), t).RoundTripper
v := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v.Set("email", "some-email@example.org")
diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go
index 27c645a94190..0b6caaa15da1 100644
--- a/selfservice/strategy/code/strategy_registration_test.go
+++ b/selfservice/strategy/code/strategy_registration_test.go
@@ -15,6 +15,8 @@ import (
"strings"
"testing"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
@@ -37,6 +39,7 @@ type state struct {
email string
testServer *httptest.Server
resultIdentity *identity.Identity
+ body string
}
func TestRegistrationCodeStrategyDisabled(t *testing.T) {
@@ -172,6 +175,7 @@ func TestRegistrationCodeStrategy(t *testing.T) {
values.Set("method", "code")
body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values))
+ s.body = body
if submitAssertion != nil {
submitAssertion(ctx, t, s, body, resp)
@@ -213,6 +217,7 @@ func TestRegistrationCodeStrategy(t *testing.T) {
vals(&values)
body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values))
+ s.body = body
if submitAssertion != nil {
submitAssertion(ctx, t, s, body, resp)
@@ -240,7 +245,7 @@ func TestRegistrationCodeStrategy(t *testing.T) {
t.Parallel()
ctx := context.Background()
- _, reg, public := setup(ctx, t)
+ conf, reg, public := setup(ctx, t)
for _, tc := range []struct {
d string
@@ -279,6 +284,15 @@ func TestRegistrationCodeStrategy(t *testing.T) {
state = submitOTP(ctx, t, reg, state, func(v *url.Values) {
v.Set("code", registrationCode)
}, tc.apiType, nil)
+
+ if tc.apiType == ApiTypeSPA {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body)
+ assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body)
+ } else if tc.apiType == ApiTypeSPA {
+ assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body)
+ } else if tc.apiType == ApiTypeNative {
+ assert.NotContains(t, gjson.Get(state.body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", state.body)
+ }
})
t.Run("case=should normalize email address on sign up", func(t *testing.T) {
diff --git a/selfservice/strategy/idfirst/.schema/login.schema.json b/selfservice/strategy/idfirst/.schema/login.schema.json
new file mode 100644
index 000000000000..02ebd4ca9d9f
--- /dev/null
+++ b/selfservice/strategy/idfirst/.schema/login.schema.json
@@ -0,0 +1,24 @@
+{
+ "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/identity_disovery/login.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "csrf_token": {
+ "type": "string"
+ },
+ "identifier": {
+ "type": "string",
+ "minLength": 1
+ },
+ "method": {
+ "type": "string",
+ "enum": [
+ "identifier_first"
+ ]
+ },
+ "transient_payload": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+}
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..086d65ade752
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "identifier_first",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "identifier_first",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "identifier_first",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070009,
+ "text": "Continue",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/schema.go b/selfservice/strategy/idfirst/schema.go
new file mode 100644
index 000000000000..77a0d37f54d7
--- /dev/null
+++ b/selfservice/strategy/idfirst/schema.go
@@ -0,0 +1,11 @@
+// Copyright © 2023 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import (
+ _ "embed"
+)
+
+//go:embed .schema/login.schema.json
+var loginSchema []byte
diff --git a/selfservice/strategy/idfirst/strategy.go b/selfservice/strategy/idfirst/strategy.go
new file mode 100644
index 000000000000..b4590ce45634
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy.go
@@ -0,0 +1,68 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import (
+ "context"
+
+ "github.com/go-playground/validator/v10"
+
+ "github.com/ory/kratos/driver/config"
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/session"
+ "github.com/ory/kratos/ui/node"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/decoderx"
+)
+
+type dependencies interface {
+ x.LoggingProvider
+ x.WriterProvider
+ x.CSRFTokenGeneratorProvider
+ x.CSRFProvider
+
+ config.Provider
+
+ identity.PrivilegedPoolProvider
+ login.StrategyProvider
+ login.FlowPersistenceProvider
+}
+
+type Strategy struct {
+ d dependencies
+ v *validator.Validate
+ hd *decoderx.HTTP
+}
+
+func NewStrategy(d any) *Strategy {
+ return &Strategy{
+ d: d.(dependencies),
+ v: validator.New(),
+ hd: decoderx.NewHTTP(),
+ }
+}
+
+func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) {
+ return 0, nil
+}
+
+func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) {
+ return 0, nil
+}
+
+func (s *Strategy) ID() identity.CredentialsType {
+ return identity.CredentialsType(node.IdentifierFirstGroup)
+}
+
+func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod {
+ return session.AuthenticationMethod{
+ Method: s.ID(),
+ AAL: identity.AuthenticatorAssuranceLevel1,
+ }
+}
+
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
+ return node.IdentifierFirstGroup
+}
diff --git a/selfservice/strategy/idfirst/strategy_login.go b/selfservice/strategy/idfirst/strategy_login.go
new file mode 100644
index 000000000000..7b4df627b6bc
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy_login.go
@@ -0,0 +1,186 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import (
+ "net/http"
+
+ "github.com/ory/kratos/schema"
+
+ "github.com/pkg/errors"
+
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/selfservice/flow"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/session"
+ "github.com/ory/kratos/text"
+ "github.com/ory/kratos/ui/node"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/decoderx"
+ "github.com/ory/x/sqlcon"
+)
+
+var (
+ _ login.FormHydrator = new(Strategy)
+ _ login.Strategy = new(Strategy)
+ ErrNoCredentialsFound = errors.New("no credentials found")
+)
+
+func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithIdentifierFirstMethod, err error) error {
+ if f != nil {
+ f.UI.Nodes.SetValueAttribute("identifier", payload.Identifier)
+ if f.Type == flow.TypeBrowser {
+ f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ }
+ }
+
+ return err
+}
+
+func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (_ *identity.Identity, err error) {
+ if !s.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(r.Context()) {
+ return nil, errors.WithStack(flow.ErrStrategyNotResponsible)
+ }
+
+ if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil {
+ return nil, err
+ }
+
+ var p updateLoginFlowWithIdentifierFirstMethod
+ if err := s.hd.Decode(r, &p,
+ decoderx.HTTPDecoderSetValidatePayloads(true),
+ decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema),
+ decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+ f.TransientPayload = p.TransientPayload
+
+ if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+
+ var opts []login.FormHydratorModifier
+
+ // Look up the user by the identifier.
+ identityHint, err := s.d.PrivilegedIdentityPool().FindIdentityByCredentialIdentifier(r.Context(), p.Identifier,
+ // We are dealing with user input -> lookup should be case-insensitive.
+ false,
+ )
+ if errors.Is(err, sqlcon.ErrNoRows) {
+ // If the user is not found, we still want to potentially show the UI for some method. That's why we don't exit here.
+ // We have to mitigate account enumeration. So we continue without setting the identity hint.
+ //
+ // This will later be handled by `didPopulate`.
+ } else if err != nil {
+ // An error happened during lookup
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ } else if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ // Hydrate credentials
+ if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(r.Context(), identityHint, identity.ExpandCredentials); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+ }
+
+ f.UI.ResetMessages()
+ f.UI.Nodes.SetValueAttribute("identifier", p.Identifier)
+
+ // Add identity hint
+ opts = append(opts, login.WithIdentityHint(identityHint))
+
+ didPopulate := false
+ for _, ls := range s.d.LoginStrategies(r.Context()) {
+ populator, ok := ls.(login.FormHydrator)
+ if !ok {
+ continue
+ }
+
+ if err := populator.PopulateLoginMethodIdentifierFirstCredentials(r, f, opts...); errors.Is(err, login.ErrBreakLoginPopulate) {
+ didPopulate = true
+ break
+ } else if errors.Is(err, ErrNoCredentialsFound) {
+ // This strategy is not responsible for this flow. We do not set didPopulate to true if that happens.
+ } else if err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ } else {
+ didPopulate = true
+ }
+ }
+
+ // If no strategy populated, it means that the account (very likely) does not exist. We show a user not found error,
+ // but only if account enumeration mitigation is disabled. Otherwise, we proceed to render the rest of the form.
+ if !didPopulate && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewAccountNotFoundError()))
+ }
+
+ // We found credentials - hide the identifier.
+ f.UI.GetNodes().RemoveMatching(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit))
+
+ // We set the identifier to hidden, so it's still available in the form but not visible to the user.
+ for k, n := range f.UI.Nodes {
+ if n.ID() != "identifier" {
+ continue
+ }
+
+ attrs, ok := f.UI.Nodes[k].Attributes.(*node.InputAttributes)
+ if !ok {
+ continue
+ }
+
+ attrs.Type = node.InputAttributeTypeHidden
+ f.UI.Nodes[k].Attributes = attrs
+ }
+
+ f.Active = s.ID()
+ if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+
+ if x.IsJSONRequest(r) {
+ s.d.Writer().WriteCode(w, r, http.StatusBadRequest, f)
+ } else {
+ http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther)
+ }
+
+ return nil, flow.ErrCompletedByStrategy
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error {
+ f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+
+ ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
+ if err != nil {
+ return err
+ }
+
+ identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
+ if err != nil {
+ return err
+ }
+
+ f.UI.SetNode(node.NewInputField("identifier", "", s.NodeGroup(), node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
+ f.UI.GetNodes().Append(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(_ *http.Request, f *login.Flow, opts ...login.FormHydratorModifier) error {
+ return ErrNoCredentialsFound
+}
+
+func (s *Strategy) RegisterLoginRoutes(_ *x.RouterPublic) {}
diff --git a/selfservice/strategy/idfirst/strategy_login_test.go b/selfservice/strategy/idfirst/strategy_login_test.go
new file mode 100644
index 000000000000..5aa88c456b25
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy_login_test.go
@@ -0,0 +1,588 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst_test
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/oidc"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/gofrs/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/tidwall/gjson"
+
+ kratos "github.com/ory/kratos/internal/httpclient"
+ "github.com/ory/kratos/text"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/assertx"
+ "github.com/ory/x/ioutilx"
+ "github.com/ory/x/urlx"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/driver/config"
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/internal"
+ "github.com/ory/kratos/internal/testhelpers"
+ "github.com/ory/kratos/selfservice/flow"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/ui/node"
+ "github.com/ory/x/snapshotx"
+)
+
+//go:embed stub/default.schema.json
+var loginSchema []byte
+
+func TestCompleteLogin(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+
+ // We enable the password method to test the identifier first strategy
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true})
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true})
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first")
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first")
+
+ router := x.NewRouterPublic()
+ publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin())
+
+ errTS := testhelpers.NewErrorTestServer(t, reg)
+ uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg)
+ redirTS := testhelpers.NewRedirSessionEchoTS(t, reg)
+
+ // Overwrite these two:
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts")
+ conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts")
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts")
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts")
+
+ // ctx = testhelpers.WithDefaultIdentitySchemaFromRaw(ctx, loginSchema)
+ testhelpers.SetDefaultIdentitySchemaFromRaw(conf, loginSchema)
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
+ conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
+
+ //ensureFieldsExist := func(t *testing.T, body []byte) {
+ // registrationhelpers.CheckFormContent(t, body, "identifier",
+ // "password",
+ // "csrf_token")
+ //}
+
+ apiClient := testhelpers.NewDebugClient(t)
+
+ t.Run("case=should show the error ui because the request payload is malformed", func(t *testing.T) {
+ t.Run("type=api", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPIWithContext(t, ctx, apiClient, publicTS, false)
+
+ body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ")
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, body, `Expected JSON sent in request body to be an object but got: Number`)
+ })
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false, testhelpers.InitFlowWithContext(ctx))
+
+ body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
+ assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts")
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body)
+ })
+
+ t.Run("type=spa", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false, testhelpers.InitFlowWithContext(ctx))
+
+ body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body)
+ })
+ })
+
+ t.Run("case=should fail because identifier first can not handle AAL2", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ update, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
+ require.NoError(t, err)
+ update.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), update))
+
+ req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(`{"method":"identifier_first"}`))
+ require.NoError(t, err)
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+
+ actual, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, req)
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.Equal(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.GetBytes(actual, "ui.messages.0.text").String())
+ })
+
+ t.Run("should return an error because the request does not exist", func(t *testing.T) {
+ check := func(t *testing.T, actual string) {
+ assert.Equal(t, int64(http.StatusNotFound), gjson.Get(actual, "code").Int(), "%s", actual)
+ assert.Equal(t, "Not Found", gjson.Get(actual, "status").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "message").String(), "Unable to locate the resource", "%s", actual)
+ }
+
+ fakeFlow := &kratos.LoginFlow{
+ Ui: kratos.UiContainer{
+ Action: publicTS.URL + login.RouteSubmitFlow + "?flow=" + x.NewUUID().String(),
+ },
+ }
+
+ t.Run("type=api", func(t *testing.T) {
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, fakeFlow, apiClient, "{}")
+ assert.Len(t, res.Cookies(), 0)
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ check(t, gjson.Get(actual, "error").Raw)
+ })
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, fakeFlow, browserClient, "")
+ assert.Contains(t, res.Request.URL.String(), errTS.URL)
+ check(t, actual)
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, fakeFlow, apiClient, "{}")
+ assert.Len(t, res.Cookies(), 0)
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ check(t, gjson.Get(actual, "error").Raw)
+ })
+ })
+
+ t.Run("case=should return an error because the request is expired", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, time.Millisecond*10)
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, time.Hour)
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ values := url.Values{
+ "csrf_token": {x.FakeCSRFToken},
+ "identifier": {"identifier"},
+ "method": {"identifier_first"},
+ }
+
+ t.Run("type=api", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPIWithContext(t, ctx, apiClient, publicTS, false)
+
+ time.Sleep(time.Millisecond * 60)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String())
+ assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since", "expired_at"}, "expired", "%s", actual)
+ })
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false)
+
+ time.Sleep(time.Millisecond * 60)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, f, browserClient, values.Encode())
+ assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts")
+ assert.NotEqual(t, f.Id, gjson.Get(actual, "id").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "expired", "%s", actual)
+ })
+
+ t.Run("type=SPA", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false)
+
+ time.Sleep(time.Millisecond * 60)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String())
+ assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since", "expired_at"}, "expired", "%s", actual)
+ })
+ })
+
+ t.Run("case=should have correct CSRF behavior", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ values := url.Values{
+ "method": {"identifier_first"},
+ "csrf_token": {"invalid_token"},
+ "identifier": {"login-identifier-csrf-browser"},
+ }
+
+ t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false)
+
+ actual, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values.Encode())
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assertx.EqualAsJSON(t, x.ErrInvalidCSRFToken,
+ json.RawMessage(actual), "%s", actual)
+ })
+
+ t.Run("case=should fail because of missing CSRF token/type=spa", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false)
+
+ actual, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values.Encode())
+ assert.EqualValues(t, http.StatusForbidden, res.StatusCode)
+ assertx.EqualAsJSON(t, x.ErrInvalidCSRFToken,
+ json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual)
+ })
+
+ t.Run("case=should pass even without CSRF token/type=api", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ actual, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
+ assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+ assert.Contains(t, actual, "1010022")
+ })
+
+ t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
+ for k, tc := range []struct {
+ mod func(http.Header)
+ exp string
+ }{
+ {
+ mod: func(h http.Header) {
+ h.Add("Cookie", "name=bar")
+ },
+ exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
+ },
+ {
+ mod: func(h http.Header) {
+ h.Add("Origin", "www.bar.com")
+ },
+ exp: "The HTTP Request Header included the \\\"Origin\\\" key",
+ },
+ } {
+ t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ req := testhelpers.NewRequest(t, true, "POST", f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values)))
+ tc.mod(req.Header)
+
+ res, err := apiClient.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+
+ actual := string(ioutilx.MustReadAll(res.Body))
+ assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+ assert.Contains(t, actual, tc.exp)
+ })
+ }
+ })
+ })
+
+ expectValidationError := func(t *testing.T, isAPI, refresh, isSPA bool, values func(url.Values)) string {
+ return testhelpers.SubmitLoginForm(t, isAPI, nil, publicTS, values,
+ isSPA, refresh,
+ testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK),
+ testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+login.RouteSubmitFlow, conf.SelfServiceFlowLoginUI(ctx).String()))
+ }
+
+ t.Run("should return an error because the user does not exist", func(t *testing.T) {
+ // In this test we check if the account mitigation behaves correctly by enabling all login strategies EXCEPT
+ // for the passwordless code strategy. That is because this strategy always shows the login button.
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{
+ {
+ ID: "google",
+ Provider: "google",
+ Label: "Google",
+ ClientID: "a",
+ ClientSecret: "b",
+ Mapper: "file://",
+ },
+ }})
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeWebAuthn.String(), true)
+ conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.display_name", "Ory Corp")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.id", "localhost")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.origin", "http://localhost:4455")
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.display_name", "Ory Corp")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.id", "localhost")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.origins", []string{"http://localhost:4455"})
+
+ t.Cleanup(func() {
+ conf.MustSet(ctx, "selfservice.methods.password", nil)
+ conf.MustSet(ctx, "selfservice.methods.oidc", nil)
+ conf.MustSet(ctx, "selfservice.methods.passkey", nil)
+ conf.MustSet(ctx, "selfservice.methods.webauthn", nil)
+ conf.MustSet(ctx, "selfservice.methods.code", nil)
+ })
+
+ t.Run("account enumeration mitigation enabled", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ check := func(t *testing.T, body string, isAPI bool) {
+ t.Logf("%s", body)
+ if !isAPI {
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWebAuthn), "we do expect to see a webauthn trigger:\n%s", body)
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPasskey), "we do expect to see a passkey trigger button:\n%s", body)
+ }
+
+ assert.Equal(t, "hidden", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).attributes.type").String(), "identifier is hidden to appear that we found an identity even though we did not")
+
+ assert.NotContains(t, body, text.NewErrorValidationAccountNotFound().Text, "we do not expect to see an account not found error:\n%s", body)
+
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPassword), "we do expect to see a password trigger:\n%s", body)
+
+ // We do expect to see the same social sign in buttons that were on the first page:
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWith), "we do expect to see a oidc trigger:\n%s", body)
+ assert.Contains(t, body, "google", "we do expect to see a google trigger:\n%s", body)
+ }
+
+ values := func(v url.Values) {
+ v.Set("identifier", "identifier")
+ v.Set("method", "identifier_first")
+ }
+
+ t.Run("type=browser", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, false, values), false)
+ })
+
+ t.Run("type=SPA", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, true, values), false)
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ check(t, expectValidationError(t, true, false, false, values), true)
+ })
+ })
+
+ t.Run("account enumeration mitigation disabled", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ check := func(t *testing.T, body string) {
+ t.Logf("%s", body)
+
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body)
+ assert.Contains(t, body, text.NewErrorValidationAccountNotFound().Text, "we do expect to see an error that the account does not exist: %s", body)
+
+ assert.Equal(t, "text", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).attributes.type").String(), "identifier is not hidden and we can see the input field as well")
+
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPasskey), "we do not expect to see a passkey trigger button: %s", body)
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWebAuthn), "we do not expect to see a webauthn trigger: %s", body)
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPassword), "we do not expect to see a password trigger: %s", body)
+
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWith), "we do not expect to see a oidc trigger: %s", body)
+ assert.NotContains(t, body, "google", "we do not expect to see a google trigger: %s", body)
+ }
+
+ values := func(v url.Values) {
+ v.Set("identifier", "identifier")
+ v.Set("method", "identifier_first")
+ }
+
+ t.Run("type=browser", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, false, values))
+ })
+
+ t.Run("type=SPA", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, true, values))
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ check(t, expectValidationError(t, true, false, false, values))
+ })
+ })
+ })
+
+ t.Run("should pass with real request", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ createIdentity(ctx, reg, t, identifier, pwd)
+
+ firstValues := func(v url.Values) {
+ v.Set("identifier", identifier)
+ v.Set("method", "identifier_first")
+ }
+
+ secondValues := func(v url.Values) {
+ v.Set("identifier", identifier)
+ v.Set("password", pwd)
+ v.Set("method", "password")
+ }
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+
+ secondStep := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, firstValues,
+ true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow)
+ t.Logf("secondStep: %s", secondStep)
+ assert.Contains(t, secondStep, "current-password")
+ assert.Contains(t, secondStep, `"value":"password"`)
+
+ body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, secondValues,
+ false, false, http.StatusOK, redirTS.URL)
+
+ assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
+ })
+
+ t.Run("type=spa", func(t *testing.T) {
+ hc := testhelpers.NewClientWithCookies(t)
+
+ secondStep := testhelpers.SubmitLoginForm(t, false, hc, publicTS, firstValues,
+ true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow)
+ t.Logf("secondStep: %s", secondStep)
+ assert.Contains(t, secondStep, "current-password")
+ assert.Contains(t, secondStep, `"value":"password"`)
+
+ body := testhelpers.SubmitLoginForm(t, false, hc, publicTS, secondValues,
+ true, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow)
+
+ assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.subject").String(), "%s", body)
+ assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body)
+ assert.Empty(t, gjson.Get(body, "session.token").String(), "%s", body)
+
+ // Was the session cookie set?
+ require.NotEmpty(t, hc.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL)), "%+v", hc.Jar)
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ secondStep := testhelpers.SubmitLoginForm(t, true, nil, publicTS, firstValues,
+ false, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow)
+ t.Logf("secondStep: %s", secondStep)
+ assert.Contains(t, secondStep, "current-password")
+ assert.Contains(t, secondStep, `"value":"password"`)
+
+ body := testhelpers.SubmitLoginForm(t, true, nil, publicTS, secondValues,
+ false, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow)
+
+ assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.subject").String(), "%s", body)
+ st := gjson.Get(body, "session_token").String()
+ assert.NotEmpty(t, st, "%s", body)
+ })
+ })
+}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first")
+
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/default.schema.json")
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsType(node.IdentifierFirstGroup))
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has password", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a password", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/idfirst/strategy_test.go b/selfservice/strategy/idfirst/strategy_test.go
new file mode 100644
index 000000000000..f6d483090abb
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy_test.go
@@ -0,0 +1,91 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/driver"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/sqlxx"
+
+ "github.com/ory/kratos/internal"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+ "github.com/ory/kratos/ui/node"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/session"
+)
+
+func TestCountActiveFirstFactorCredentials(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+ cc := make(map[identity.CredentialsType]identity.Credentials)
+
+ count, err := s.CountActiveFirstFactorCredentials(cc)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, count)
+}
+
+func TestCountActiveMultiFactorCredentials(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+ cc := make(map[identity.CredentialsType]identity.Credentials)
+
+ count, err := s.CountActiveMultiFactorCredentials(cc)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, count)
+}
+
+func TestCompletedAuthenticationMethod(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+ ctx := context.Background()
+
+ method := s.CompletedAuthenticationMethod(ctx, session.AuthenticationMethods{})
+ assert.Equal(t, s.ID(), method.Method)
+ assert.Equal(t, identity.AuthenticatorAssuranceLevel1, method.AAL)
+}
+
+func TestNodeGroup(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+
+ group := s.NodeGroup()
+ assert.Equal(t, node.IdentifierFirstGroup, group)
+}
+
+func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) *identity.Identity {
+ p, _ := reg.Hasher(ctx).Generate(context.Background(), []byte(password))
+ iId := x.NewUUID()
+ id := &identity.Identity{
+ ID: iId,
+ Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
+ Credentials: map[identity.CredentialsType]identity.Credentials{
+ identity.CredentialsTypePassword: {
+ Type: identity.CredentialsTypePassword,
+ Identifiers: []string{identifier},
+ Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`),
+ },
+ },
+ VerifiableAddresses: []identity.VerifiableAddress{
+ {
+ ID: x.NewUUID(),
+ Value: identifier,
+ Verified: false,
+ CreatedAt: time.Now(),
+ IdentityID: iId,
+ },
+ },
+ }
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), id))
+ return id
+}
diff --git a/selfservice/strategy/idfirst/stub/default.schema.json b/selfservice/strategy/idfirst/stub/default.schema.json
new file mode 100644
index 000000000000..8dc923266050
--- /dev/null
+++ b/selfservice/strategy/idfirst/stub/default.schema.json
@@ -0,0 +1,29 @@
+{
+ "$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",
+ "ory.sh/kratos": {
+ "credentials": {
+ "password": {
+ "identifier": true
+ }
+ },
+ "verification": {
+ "via": "email"
+ },
+ "recovery": {
+ "via": "email"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/selfservice/strategy/idfirst/types.go b/selfservice/strategy/idfirst/types.go
new file mode 100644
index 000000000000..a8838043782a
--- /dev/null
+++ b/selfservice/strategy/idfirst/types.go
@@ -0,0 +1,29 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import "encoding/json"
+
+// Update Login Flow with Multi-Step Method
+//
+// swagger:model updateLoginFlowWithIdentifierFirstMethod
+type updateLoginFlowWithIdentifierFirstMethod struct {
+ // Method should be set to "password" when logging in using the identifier and password strategy.
+ //
+ // required: true
+ Method string `json:"method"`
+
+ // Sending the anti-csrf token is only required for browser login flows.
+ CSRFToken string `json:"csrf_token"`
+
+ // Identifier is the email or username of the user trying to log in.
+ //
+ // required: true
+ Identifier string `json:"identifier"`
+
+ // Transient data to pass along to any webhooks
+ //
+ // required: false
+ TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
+}
diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
index 3bb3cbbf3ef6..5ac9946936c8 100644
--- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
+++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
index 498575cfee1b..1a8d048fe37d 100644
--- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
+++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
@@ -45,8 +45,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
index 3bb3cbbf3ef6..5ac9946936c8 100644
--- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
+++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
index 498575cfee1b..1a8d048fe37d 100644
--- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
+++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
@@ -45,8 +45,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go
index 184399ca1002..e6d91051c2c4 100644
--- a/selfservice/strategy/link/strategy_recovery.go
+++ b/selfservice/strategy/link/strategy_recovery.go
@@ -56,7 +56,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err
// v0.5: form.Field{Name: "email", Type: "email", Required: true},
node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
)
- f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
return nil
}
diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go
index 7b56ca5f1728..6b9240a4325b 100644
--- a/selfservice/strategy/link/strategy_recovery_test.go
+++ b/selfservice/strategy/link/strategy_recovery_test.go
@@ -369,7 +369,7 @@ func TestRecovery(t *testing.T) {
v.Set("email", "some-email@example.org")
v.Set("method", "link")
- authClient := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ authClient := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
if isAPI {
req := httptest.NewRequest("GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
@@ -380,7 +380,7 @@ func TestRecovery(t *testing.T) {
identity.AuthenticatorAssuranceLevel1,
)
require.NoError(t, err)
- authClient = testhelpers.NewHTTPClientWithSessionCookieLocalhost(t, reg, s)
+ authClient = testhelpers.NewHTTPClientWithSessionCookieLocalhost(t, ctx, reg, s)
}
body, res := testhelpers.RecoveryMakeRequest(t, isAPI || isSPA, f, authClient, testhelpers.EncodeFormAsJSON(t, isAPI || isSPA, v))
@@ -675,7 +675,7 @@ func TestRecovery(t *testing.T) {
v.Set("email", email)
}
- cl := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ cl := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
check(t, expectSuccess(t, nil, false, false, values), email, cl, func(_ *http.Client, req *http.Request) (*http.Response, error) {
_, res := testhelpers.MockMakeAuthenticatedRequestWithClientAndID(t, reg, conf, publicRouter.Router, req, cl, id)
return res, nil
diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go
index a2a72ea9a277..61f95da52fef 100644
--- a/selfservice/strategy/link/strategy_verification.go
+++ b/selfservice/strategy/link/strategy_verification.go
@@ -44,7 +44,7 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F
// v0.5: form.Field{Name: "email", Type: "email", Required: true}
node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
)
- f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
return nil
}
diff --git a/selfservice/strategy/lookup/login_test.go b/selfservice/strategy/lookup/login_test.go
index c4896962c660..c746d744f059 100644
--- a/selfservice/strategy/lookup/login_test.go
+++ b/selfservice/strategy/lookup/login_test.go
@@ -14,6 +14,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -55,7 +57,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=lookup payload is set when identity has lookup", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
})
@@ -63,7 +65,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
@@ -71,7 +73,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
@@ -86,7 +88,7 @@ func TestCompleteLogin(t *testing.T) {
}
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
return doAPIFlowWithClient(t, v, id, apiClient, false)
}
@@ -99,7 +101,7 @@ func TestCompleteLogin(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
return doBrowserFlowWithClient(t, spa, v, id, browserClient, false)
}
@@ -235,30 +237,35 @@ func TestCompleteLogin(t *testing.T) {
}
t.Run("type=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
body, res := doAPIFlowWithClient(t, payload("key-0"), id, apiClient, false)
check(t, false, body, res, "key-0", 2)
// We can still use another key
body, res = doAPIFlowWithClient(t, payload("key-2"), id, apiClient, true)
check(t, false, body, res, "key-2", 3)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=browser", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
body, res := doBrowserFlowWithClient(t, false, payload("key-3"), id, browserClient, false)
check(t, true, body, res, "key-3", 2)
// We can still use another key
body, res = doBrowserFlowWithClient(t, false, payload("key-5"), id, browserClient, true)
check(t, true, body, res, "key-5", 3)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=spa", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
body, res := doBrowserFlowWithClient(t, true, payload("key-6"), id, browserClient, false)
check(t, false, body, res, "key-6", 2)
// We can still use another key
body, res = doBrowserFlowWithClient(t, true, payload("key-8"), id, browserClient, true)
check(t, false, body, res, "key-8", 3)
+
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body)
})
})
diff --git a/selfservice/strategy/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go
index fce2be4c0974..101f5919d04a 100644
--- a/selfservice/strategy/lookup/settings_test.go
+++ b/selfservice/strategy/lookup/settings_test.go
@@ -111,7 +111,7 @@ func TestCompleteSettings(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -120,7 +120,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -129,7 +129,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=hide recovery codes behind reveal button and show disable button", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
t.Run("case=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS)
@@ -142,7 +142,7 @@ func TestCompleteSettings(t *testing.T) {
})
t.Run("case=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
})
@@ -150,7 +150,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=button for regeneration is displayed when identity has no recovery codes yet", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
t.Run("case=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS)
@@ -163,7 +163,7 @@ func TestCompleteSettings(t *testing.T) {
})
t.Run("case=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
})
@@ -389,7 +389,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -410,7 +410,7 @@ func TestCompleteSettings(t *testing.T) {
runBrowser := func(t *testing.T, spa bool) {
id, _ := createIdentity(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -423,8 +423,11 @@ func TestCompleteSettings(t *testing.T) {
if spa {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
} else {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
}
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
@@ -480,7 +483,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -498,7 +501,7 @@ func TestCompleteSettings(t *testing.T) {
runBrowser := func(t *testing.T, spa bool) {
id, _ := createIdentity(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -508,8 +511,11 @@ func TestCompleteSettings(t *testing.T) {
if spa {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
} else {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
}
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1,37 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "oidc",
+ "attributes": {
+ "name": "provider",
+ "type": "submit",
+ "value": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1,37 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "oidc",
+ "attributes": {
+ "name": "provider",
+ "type": "submit",
+ "value": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json
new file mode 100644
index 000000000000..29f5e9aa1061
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json
@@ -0,0 +1,24 @@
+[
+ {
+ "type": "input",
+ "group": "oidc",
+ "attributes": {
+ "name": "provider",
+ "type": "submit",
+ "value": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1,37 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "oidc",
+ "attributes": {
+ "name": "provider",
+ "type": "submit",
+ "value": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
@@ -0,0 +1,37 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "oidc",
+ "attributes": {
+ "name": "provider",
+ "type": "submit",
+ "value": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
new file mode 100644
index 000000000000..364b8abc331c
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
@@ -0,0 +1,15 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json
deleted file mode 100644
index bacf802cd191..000000000000
--- a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "method": "POST",
- "nodes": [
- {
- "type": "input",
- "group": "default",
- "attributes": {
- "name": "csrf_token",
- "type": "hidden",
- "required": true,
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {}
- },
- {
- "type": "input",
- "group": "oidc",
- "attributes": {
- "name": "provider",
- "type": "submit",
- "value": "valid",
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {
- "label": {
- "id": 1010002,
- "text": "Sign in with valid",
- "type": "info",
- "context": {
- "provider": "valid"
- }
- }
- }
- },
- {
- "type": "input",
- "group": "oidc",
- "attributes": {
- "name": "provider",
- "type": "submit",
- "value": "secondProvider",
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {
- "label": {
- "id": 1010002,
- "text": "Sign in with secondProvider",
- "type": "info",
- "context": {
- "provider": "secondProvider"
- }
- }
- }
- },
- {
- "type": "input",
- "group": "oidc",
- "attributes": {
- "name": "provider",
- "type": "submit",
- "value": "claimsViaUserInfo",
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {
- "label": {
- "id": 1010002,
- "text": "Sign in with claimsViaUserInfo",
- "type": "info",
- "context": {
- "provider": "claimsViaUserInfo"
- }
- }
- }
- },
- {
- "type": "input",
- "group": "oidc",
- "attributes": {
- "name": "provider",
- "type": "submit",
- "value": "invalid-issuer",
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {
- "label": {
- "id": 1010002,
- "text": "Sign in with invalid-issuer",
- "type": "info",
- "context": {
- "provider": "invalid-issuer"
- }
- }
- }
- }
- ]
-}
diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go
index 6515d06367ee..102339b77991 100644
--- a/selfservice/strategy/oidc/strategy.go
+++ b/selfservice/strategy/oidc/strategy.go
@@ -24,7 +24,6 @@ import (
"golang.org/x/oauth2"
"github.com/ory/kratos/cipher"
- "github.com/ory/kratos/selfservice/flowhelpers"
"github.com/ory/kratos/selfservice/sessiontokenexchange"
"github.com/ory/x/jsonnetsecure"
"github.com/ory/x/otelx"
@@ -119,9 +118,9 @@ type Dependencies interface {
func isForced(req interface{}) bool {
f, ok := req.(interface {
- IsForced() bool
+ IsRefresh() bool
})
- return ok && f.IsForced()
+ return ok && f.IsRefresh()
}
// Strategy implements selfservice.LoginStrategy, selfservice.RegistrationStrategy and selfservice.SettingsStrategy.
@@ -537,38 +536,8 @@ func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(pro
return err
}
- providers := conf.Providers
-
- if lf, ok := f.(*login.Flow); ok && lf.IsForced() {
- if _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()); id != nil {
- if c == nil {
- // no OIDC credentials, don't add any providers
- providers = nil
- } else {
- var credentials identity.CredentialsOIDC
- if err := json.Unmarshal(c.Config, &credentials); err != nil {
- // failed to read OIDC credentials, don't add any providers
- providers = nil
- } else {
- // add only providers that can actually be used to log in as this identity
- providers = make([]Configuration, 0, len(conf.Providers))
- for i := range conf.Providers {
- for j := range credentials.Providers {
- if conf.Providers[i].ID == credentials.Providers[j].Provider {
- providers = append(providers, conf.Providers[i])
- break
- }
- }
- }
- }
- }
- }
- }
-
- // does not need sorting because there is only one field
- c := f.GetUI()
- c.SetCSRF(s.d.GenerateCSRFToken(r))
- AddProviders(c, providers, message)
+ f.GetUI().SetCSRF(s.d.GenerateCSRFToken(r))
+ AddProviders(f.GetUI(), conf.Providers, message)
return nil
}
diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go
index 42b948ec7c11..3b5f72291704 100644
--- a/selfservice/strategy/oidc/strategy_login.go
+++ b/selfservice/strategy/oidc/strategy_login.go
@@ -10,6 +10,11 @@ import (
"strings"
"time"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+ "github.com/ory/x/stringsx"
+
+ "github.com/ory/kratos/selfservice/flowhelpers"
+
"github.com/julienschmidt/httprouter"
"github.com/ory/kratos/session"
@@ -34,21 +39,15 @@ import (
"github.com/ory/kratos/x"
)
-var _ login.Strategy = new(Strategy)
+var (
+ _ login.FormHydrator = new(Strategy)
+ _ login.Strategy = new(Strategy)
+)
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
s.setRoutes(r)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, l *login.Flow) error {
- // This strategy can only solve AAL1
- if requestedAAL > identity.AuthenticatorAssuranceLevel1 {
- return nil
- }
-
- return s.populateMethod(r, l, text.NewInfoLoginWith)
-}
-
// Update Login Flow with OpenID Connect Method
//
// swagger:model updateLoginFlowWithOidcMethod
@@ -290,3 +289,97 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
return nil, errors.WithStack(flow.ErrCompletedByStrategy)
}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, lf *login.Flow) error {
+ conf, err := s.Config(r.Context())
+ if err != nil {
+ return err
+ }
+
+ var providers []Configuration
+ _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID())
+ if id == nil || c == nil {
+ providers = nil
+ } else {
+ var credentials identity.CredentialsOIDC
+ if err := json.Unmarshal(c.Config, &credentials); err != nil {
+ // failed to read OIDC credentials, don't add any providers
+ providers = nil
+ } else {
+ // add only providers that can actually be used to log in as this identity
+ providers = make([]Configuration, 0, len(conf.Providers))
+ for i := range conf.Providers {
+ for j := range credentials.Providers {
+ if conf.Providers[i].ID == credentials.Providers[j].Provider {
+ providers = append(providers, conf.Providers[i])
+ break
+ }
+ }
+ }
+ }
+ }
+
+ lf.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ AddProviders(lf.UI, providers, text.NewInfoLoginWith)
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error {
+ return s.populateMethod(r, f, text.NewInfoLoginWith)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, f *login.Flow, mods ...login.FormHydratorModifier) error {
+ conf, err := s.Config(r.Context())
+ if err != nil {
+ return err
+ }
+
+ o := login.NewFormHydratorOptions(mods)
+
+ var linked []Provider
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we check if the identity has any providers configured.
+ if linked, err = s.linkedProviders(r.Context(), r, conf, o.IdentityHint); err != nil {
+ return err
+ }
+ }
+
+ if len(linked) == 0 {
+ // If we found no credentials:
+ if s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ // We found no credentials but do not want to leak that we know that. So we return early and do not
+ // modify the initial provider list.
+ return nil
+ }
+
+ // We found no credentials. We remove all the providers and tell the strategy that we found nothing.
+ f.GetUI().UnsetNode("provider")
+ return idfirst.ErrNoCredentialsFound
+ }
+
+ if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ // Account enumeration is disabled, so we show all providers that are linked to the identity.
+ // User is found and enumeration mitigation is disabled. Filter the list!
+ f.GetUI().UnsetNode("provider")
+
+ for _, l := range linked {
+ lc := l.Config()
+ AddProvider(f.UI, lc.ID, text.NewInfoLoginWith(stringsx.Coalesce(lc.Label, lc.ID)))
+ }
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error {
+ return s.populateMethod(r, f, text.NewInfoLoginWith)
+}
diff --git a/selfservice/strategy/oidc/strategy_login_test.go b/selfservice/strategy/oidc/strategy_login_test.go
new file mode 100644
index 000000000000..074019dedb17
--- /dev/null
+++ b/selfservice/strategy/oidc/strategy_login_test.go
@@ -0,0 +1,160 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package oidc_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/gofrs/uuid"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/driver"
+ "github.com/ory/kratos/driver/config"
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/internal"
+ "github.com/ory/kratos/internal/testhelpers"
+ "github.com/ory/kratos/selfservice/flow"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/snapshotx"
+)
+
+func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry, id uuid.UUID, provider string) *identity.Identity {
+ creds, err := identity.NewCredentialsOIDC(new(identity.CredentialsOIDCEncryptedTokens), provider, id.String(), "")
+ require.NoError(t, err)
+
+ i := identity.NewIdentity("default")
+ i.SetCredentials(identity.CredentialsTypeOIDC, *creds)
+
+ require.NoError(t, reg.IdentityManager().Create(ctx, i))
+ return i
+}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ providerID := "test-provider"
+
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".enabled", true)
+ ctx = configtesthelpers.WithConfigValue(
+ ctx,
+ config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config",
+ map[string]interface{}{
+ "providers": []map[string]interface{}{
+ {
+ "provider": "generic",
+ "id": providerID,
+ "client_id": "invalid",
+ "client_secret": "invalid",
+ "issuer_url": "https://foobar/",
+ "mapper_url": "file://./stub/oidc.facebook.jsonnet",
+ },
+ },
+ },
+ )
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/stub.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+
+ id := createIdentity(t, ctx, reg, x.NewUUID(), providerID)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity(providerID)
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has oidc", func(t *testing.T) {
+ identifier := x.NewUUID()
+ id := createIdentity(t, ctx, reg, identifier, providerID)
+
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a oidc", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go
index 65e5ab30600c..08485ee19222 100644
--- a/selfservice/strategy/oidc/strategy_settings_test.go
+++ b/selfservice/strategy/oidc/strategy_settings_test.go
@@ -16,8 +16,8 @@ import (
"github.com/ory/x/snapshotx"
- "github.com/ory/kratos/driver"
kratos "github.com/ory/kratos/internal/httpclient"
+ "github.com/ory/kratos/driver"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/ui/node"
diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go
index 65c8f09b2e06..0f41fd622d0e 100644
--- a/selfservice/strategy/oidc/strategy_test.go
+++ b/selfservice/strategy/oidc/strategy_test.go
@@ -1415,16 +1415,6 @@ func TestStrategy(t *testing.T) {
snapshotx.SnapshotTExcept(t, sr.UI, []string{"action", "nodes.0.attributes.value"})
})
-
- t.Run("method=TestPopulateLoginMethod", func(t *testing.T) {
- conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/")
-
- sr, err := login.NewFlow(conf, time.Minute, "nosurf", &http.Request{URL: urlx.ParseOrPanic("/")}, flow.TypeBrowser)
- require.NoError(t, err)
- require.NoError(t, reg.LoginStrategies(context.Background()).MustStrategy(identity.CredentialsTypeOIDC).(*oidc.Strategy).PopulateLoginMethod(&http.Request{}, identity.AuthenticatorAssuranceLevel1, sr))
-
- snapshotx.SnapshotTExcept(t, sr.UI, []string{"action", "nodes.0.attributes.value"})
- })
}
func prettyJSON(t *testing.T, body []byte) string {
@@ -1533,7 +1523,7 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) {
func TestDisabledEndpoint(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), false)
-
+ ctx := context.Background()
publicTS, _ := testhelpers.NewKratosServer(t, reg)
t.Run("case=should not callback when oidc method is disabled", func(t *testing.T) {
@@ -1551,7 +1541,7 @@ func TestDisabledEndpoint(t *testing.T) {
t.Run("flow=settings", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/stub.schema.json")
- c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
f := testhelpers.InitializeSettingsFlowViaAPI(t, c, publicTS)
res, err := c.PostForm(f.Ui.Action, url.Values{"link": {"oidc"}})
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json
index d2dd6567d240..ffb5ec222642 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json
@@ -38,7 +38,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -53,8 +53,10 @@
"disabled": false,
"name": "passkey_login_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyLogin()",
- "onload": "window.__oryPasskeyLoginAutocompleteInit()",
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
"type": "button",
"value": ""
},
@@ -74,6 +76,8 @@
"disabled": false,
"name": "passkey_login",
"node_type": "input",
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
"type": "hidden"
},
"group": "passkey",
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json
index c331d4f4280f..1e026fb9979a 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json
@@ -30,7 +30,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -45,7 +45,8 @@
"disabled": false,
"name": "passkey_login_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyLogin()",
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
"type": "button",
"value": ""
},
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json
index c331d4f4280f..1e026fb9979a 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json
@@ -30,7 +30,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -45,7 +45,8 @@
"disabled": false,
"name": "passkey_login_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyLogin()",
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
"type": "button",
"value": ""
},
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
index f9032e39049d..a0383567eda4 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
@@ -4,7 +4,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeySettingsRegistration()",
+ "onclick": "window.oryPasskeySettingsRegistration()",
+ "onclickTrigger": "oryPasskeySettingsRegistration",
"type": "button",
"value": ""
},
@@ -109,7 +110,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
index 7e5c5b3d082b..8d91edf04ce5 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
@@ -4,7 +4,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeySettingsRegistration()",
+ "onclick": "window.oryPasskeySettingsRegistration()",
+ "onclickTrigger": "oryPasskeySettingsRegistration",
"type": "button",
"value": ""
},
@@ -61,7 +62,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..3e9aa5b5199e
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1,100 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "autocomplete": "username webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_challenge",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login",
+ "type": "hidden",
+ "disabled": false,
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..33d9f8afd952
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1,88 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_challenge",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "value": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..222443d4988b
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1,77 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "autocomplete": "username webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_challenge",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login",
+ "type": "hidden",
+ "disabled": false,
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
@@ -0,0 +1 @@
+[]
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
index 18e0cda77811..e4c5160c9697 100644
--- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json
+++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json
@@ -43,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -70,7 +70,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyRegistration()",
+ "onclick": "window.oryPasskeyRegistration()",
+ "onclickTrigger": "oryPasskeyRegistration",
"type": "button"
},
"group": "passkey",
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
index 18e0cda77811..e4c5160c9697 100644
--- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json
+++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json
@@ -43,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -70,7 +70,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyRegistration()",
+ "onclick": "window.oryPasskeyRegistration()",
+ "onclickTrigger": "oryPasskeyRegistration",
"type": "button"
},
"group": "passkey",
diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go
index 63d5ec66f2f0..857d6e824d32 100644
--- a/selfservice/strategy/passkey/passkey_login.go
+++ b/selfservice/strategy/passkey/passkey_login.go
@@ -9,6 +9,10 @@ import (
"net/http"
"strings"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
@@ -29,23 +33,13 @@ import (
"github.com/ory/x/decoderx"
)
+var _ login.FormHydrator = new(Strategy)
+
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
webauthnx.RegisterWebauthnRoute(r)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, aal identity.AuthenticatorAssuranceLevel, sr *login.Flow) error {
- if sr.Type != flow.TypeBrowser || aal != identity.AuthenticatorAssuranceLevel1 {
- return nil
- }
-
- return s.populateLoginMethodForPasskeys(r, sr)
-}
-
func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *login.Flow) error {
- if loginFlow.IsForced() {
- return s.populateLoginMethodForRefresh(r, loginFlow)
- }
-
ctx := r.Context()
loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
@@ -100,7 +94,8 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo
Name: node.PasskeyChallenge,
Type: node.InputAttributeTypeHidden,
FieldValue: string(injectWebAuthnOptions),
- }})
+ },
+ })
loginFlow.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
@@ -109,122 +104,12 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo
Group: node.PasskeyGroup,
Meta: &node.Meta{},
Attributes: &node.InputAttributes{
- Name: node.PasskeyLogin,
- Type: node.InputAttributeTypeHidden,
- }})
-
- loginFlow.UI.Nodes.Append(node.NewInputField(
- node.PasskeyLoginTrigger,
- "",
- node.PasskeyGroup,
- node.InputAttributeTypeButton,
- node.WithInputAttributes(func(attr *node.InputAttributes) {
- attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js
- attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here
- }),
- ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
-
- return nil
-}
-
-func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *login.Flow) error {
- ctx := r.Context()
-
- identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, loginFlow, s.ID())
- if identifier == "" {
- return nil
- }
-
- id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID)
- if err != nil {
- return err
- }
-
- cred, ok := id.GetCredentials(s.ID())
- if !ok {
- // Identity has no passkey
- return nil
- }
-
- var conf identity.CredentialsWebAuthnConfig
- if err := json.Unmarshal(cred.Config, &conf); err != nil {
- return errors.WithStack(err)
- }
-
- webAuthCreds := conf.Credentials.ToWebAuthn()
- if len(webAuthCreds) == 0 {
- // Identity has no webauthn
- return nil
- }
-
- passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id)
-
- webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx))
- if err != nil {
- return errors.WithStack(err)
- }
- option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{
- Name: passkeyIdentifier,
- ID: conf.UserHandle,
- Credentials: webAuthCreds,
- Config: webAuthn.Config,
+ Name: node.PasskeyLogin,
+ Type: node.InputAttributeTypeHidden,
+ OnLoad: js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()",
+ OnLoadTrigger: js.WebAuthnTriggersPasskeyLoginAutocompleteInit,
+ },
})
- if err != nil {
- return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error()))
- }
-
- loginFlow.InternalContext, err = sjson.SetBytes(
- loginFlow.InternalContext,
- flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData),
- sessionData,
- )
- if err != nil {
- return errors.WithStack(err)
- }
-
- injectWebAuthnOptions, err := json.Marshal(option)
- if err != nil {
- return errors.WithStack(err)
- }
-
- loginFlow.UI.Nodes.Upsert(&node.Node{
- Type: node.Input,
- Group: node.PasskeyGroup,
- Meta: &node.Meta{},
- Attributes: &node.InputAttributes{
- Name: node.PasskeyChallenge,
- Type: node.InputAttributeTypeHidden,
- FieldValue: string(injectWebAuthnOptions),
- }})
-
- loginFlow.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
-
- loginFlow.UI.Nodes.Upsert(&node.Node{
- Type: node.Input,
- Group: node.PasskeyGroup,
- Meta: &node.Meta{},
- Attributes: &node.InputAttributes{
- Name: node.PasskeyLogin,
- Type: node.InputAttributeTypeHidden,
- }})
-
- loginFlow.UI.Nodes.Append(node.NewInputField(
- node.PasskeyLoginTrigger,
- "",
- node.PasskeyGroup,
- node.InputAttributeTypeButton,
- node.WithInputAttributes(func(attr *node.InputAttributes) {
- attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js
- }),
- ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
-
- loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- loginFlow.UI.SetNode(node.NewInputField(
- "identifier",
- passkeyIdentifier,
- node.DefaultGroup,
- node.InputAttributeTypeHidden,
- ))
return nil
}
@@ -393,3 +278,200 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f *
return i, nil
}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow) error {
+ if f.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ ctx := r.Context()
+
+ identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, f, s.ID())
+ if identifier == "" {
+ return nil
+ }
+
+ id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID)
+ if err != nil {
+ return err
+ }
+
+ cred, ok := id.GetCredentials(s.ID())
+ if !ok {
+ // Identity has no passkey
+ return nil
+ }
+
+ var conf identity.CredentialsWebAuthnConfig
+ if err := json.Unmarshal(cred.Config, &conf); err != nil {
+ return errors.WithStack(err)
+ }
+
+ webAuthCreds := conf.Credentials.ToWebAuthn()
+ if len(webAuthCreds) == 0 {
+ // Identity has no webauthn
+ return nil
+ }
+
+ passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id)
+
+ webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx))
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{
+ Name: passkeyIdentifier,
+ ID: conf.UserHandle,
+ Credentials: webAuthCreds,
+ Config: webAuthn.Config,
+ })
+ if err != nil {
+ return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error()))
+ }
+
+ f.InternalContext, err = sjson.SetBytes(
+ f.InternalContext,
+ flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData),
+ sessionData,
+ )
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ injectWebAuthnOptions, err := json.Marshal(option)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ f.UI.Nodes.Upsert(&node.Node{
+ Type: node.Input,
+ Group: node.PasskeyGroup,
+ Meta: &node.Meta{},
+ Attributes: &node.InputAttributes{
+ Name: node.PasskeyChallenge,
+ Type: node.InputAttributeTypeHidden,
+ FieldValue: string(injectWebAuthnOptions),
+ },
+ })
+
+ f.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
+
+ f.UI.Nodes.Upsert(&node.Node{
+ Type: node.Input,
+ Group: node.PasskeyGroup,
+ Meta: &node.Meta{},
+ Attributes: &node.InputAttributes{
+ Name: node.PasskeyLogin,
+ Type: node.InputAttributeTypeHidden,
+ },
+ })
+
+ f.UI.Nodes.Append(node.NewInputField(
+ node.PasskeyLoginTrigger,
+ "",
+ node.PasskeyGroup,
+ node.InputAttributeTypeButton,
+ node.WithInputAttributes(func(attr *node.InputAttributes) {
+ //nolint:staticcheck
+ attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js
+ attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin
+ }),
+ ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
+
+ f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ f.UI.SetNode(node.NewInputField(
+ "identifier",
+ passkeyIdentifier,
+ node.DefaultGroup,
+ node.InputAttributeTypeHidden,
+ ))
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error {
+ if f.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ if err := s.populateLoginMethodForPasskeys(r, f); err != nil {
+ return err
+ }
+
+ f.UI.Nodes.Append(node.NewInputField(
+ node.PasskeyLoginTrigger,
+ "",
+ node.PasskeyGroup,
+ node.InputAttributeTypeButton,
+ node.WithInputAttributes(func(attr *node.InputAttributes) {
+ //nolint:staticcheck
+ attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js
+ attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin
+
+ //nolint:staticcheck
+ attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here
+ attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit
+ }),
+ ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error {
+ if sr.Type != flow.TypeBrowser {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ o := login.NewFormHydratorOptions(opts)
+
+ var count int
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we can perform identity credentials discovery and
+ // hide this credential if it should not be included.
+ count, err = s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials)
+ if err != nil {
+ return err
+ }
+ }
+
+ if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ sr.UI.Nodes.Append(node.NewInputField(
+ node.PasskeyLoginTrigger,
+ "",
+ node.PasskeyGroup,
+ node.InputAttributeTypeButton,
+ node.WithInputAttributes(func(attr *node.InputAttributes) {
+ //nolint:staticcheck
+ attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js
+ attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin
+ }),
+ ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
+ }
+
+ if count == 0 {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ if err := s.populateLoginMethodForPasskeys(r, sr); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go
index cae6aa0ee4dd..db2a42190f9e 100644
--- a/selfservice/strategy/passkey/passkey_login_test.go
+++ b/selfservice/strategy/passkey/passkey_login_test.go
@@ -8,22 +8,31 @@ import (
_ "embed"
"encoding/json"
"net/http"
+ "net/http/httptest"
"net/url"
"testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
+ "github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
+ "github.com/ory/kratos/internal"
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"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/snapshotx"
)
@@ -45,12 +54,12 @@ func TestPopulateLoginMethod(t *testing.T) {
t.Run("case=should not handle AAL2", func(t *testing.T) {
loginFlow := &login.Flow{Type: flow.TypeBrowser}
- assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel2, loginFlow))
+ assert.Nil(t, s.PopulateLoginMethodSecondFactor(nil, loginFlow))
})
t.Run("case=should not handle API flows", func(t *testing.T) {
loginFlow := &login.Flow{Type: flow.TypeAPI}
- assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel1, loginFlow))
+ assert.Nil(t, s.PopulateLoginMethodFirstFactor(nil, loginFlow))
})
}
@@ -209,7 +218,14 @@ func TestCompleteLogin(t *testing.T) {
actualFlow, err := fix.reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
+
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData)))
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
// We test here that login works even if the identity schema contains
@@ -233,8 +249,8 @@ func TestCompleteLogin(t *testing.T) {
fix.conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1")
loginFixtureSuccessEmail := gjson.GetBytes(loginSuccessIdentity, "traits.email").String()
- run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) {
- body, res, f := fix.submitWebAuthnLogin(t, isSPA, id, context, func(values url.Values) {
+ run := func(t *testing.T, ctx context.Context, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) {
+ body, res, f := fix.submitWebAuthnLogin(t, ctx, isSPA, id, context, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.PasskeyLogin, string(response))
}, testhelpers.InitFlowWithRefresh())
@@ -290,10 +306,150 @@ func TestCompleteLogin(t *testing.T) {
"spa",
} {
t.Run(f, func(t *testing.T) {
- run(t, id, tc.context, tc.response, f == "spa", expectedAAL)
+ run(t, ctx, id, tc.context, tc.response, f == "spa", expectedAAL)
})
}
})
}
})
}
+
+func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry, id uuid.UUID) *identity.Identity {
+ i := identity.NewIdentity("default")
+ i.SetCredentials(identity.CredentialsTypePasskey, identity.Credentials{
+ Identifiers: []string{id.String()},
+ Config: loginPasswordlessCredentials,
+ Type: identity.CredentialsTypePasskey,
+ Version: 1,
+ })
+
+ require.NoError(t, reg.IdentityManager().Create(ctx, i))
+ return i
+}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true)
+ ctx = configtesthelpers.WithConfigValue(
+ ctx,
+ config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config",
+ map[string]interface{}{
+ "rp": map[string]interface{}{
+ "display_name": "foo",
+ "id": "localhost",
+ "origins": []string{"http://localhost"},
+ },
+ },
+ )
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/login.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePasskey)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ f.UI.Nodes.ResetNodes("passkey_challenge")
+ snapshotx.SnapshotT(t, f.UI.Nodes, snapshotx.ExceptNestedKeys("nonce", "src"))
+ }
+
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ f.UI.Nodes = make(node.Nodes, 0)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+
+ id := createIdentity(t, ctx, reg, x.NewUUID())
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("test-provider")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has passkey", func(t *testing.T) {
+ identifier := x.NewUUID()
+ id := createIdentity(t, ctx, reg, identifier)
+
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a passkey", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/passkey/passkey_registration.go b/selfservice/strategy/passkey/passkey_registration.go
index 88efd420d725..9be753f70c40 100644
--- a/selfservice/strategy/passkey/passkey_registration.go
+++ b/selfservice/strategy/passkey/passkey_registration.go
@@ -11,6 +11,8 @@ import (
"net/url"
"strings"
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
@@ -280,9 +282,10 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registra
Group: node.PasskeyGroup,
Meta: &node.Meta{Label: text.NewInfoSelfServiceRegistrationRegisterPasskey()},
Attributes: &node.InputAttributes{
- Name: node.PasskeyRegisterTrigger,
- Type: node.InputAttributeTypeButton,
- OnClick: "window.__oryPasskeyRegistration()", // defined in webauthn.js
+ Name: node.PasskeyRegisterTrigger,
+ Type: node.InputAttributeTypeButton,
+ OnClick: js.WebAuthnTriggersPasskeyRegistration.String() + "()", // defined in webauthn.js
+ OnClickTrigger: js.WebAuthnTriggersPasskeyRegistration,
}})
// Passkey nodes end
diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go
index d495e8c4dfe4..d7191207cedb 100644
--- a/selfservice/strategy/passkey/passkey_registration_test.go
+++ b/selfservice/strategy/passkey/passkey_registration_test.go
@@ -8,6 +8,8 @@ import (
"net/url"
"testing"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
@@ -327,6 +329,13 @@ func TestRegistration(t *testing.T) {
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)
+
+ 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(), fix.redirNoSessionTS.URL+"/registration-return-ts", "%s", actual)
+ } else {
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
+ }
})
}
})
diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go
index 548a261e442b..89423bf8adeb 100644
--- a/selfservice/strategy/passkey/passkey_settings.go
+++ b/selfservice/strategy/passkey/passkey_settings.go
@@ -11,6 +11,8 @@ import (
"strings"
"time"
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/gofrs/uuid"
@@ -114,7 +116,9 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity
node.PasskeyGroup,
node.InputAttributeTypeButton,
node.WithInputAttributes(func(a *node.InputAttributes) {
- a.OnClick = "window.__oryPasskeySettingsRegistration()"
+ //nolint:staticcheck
+ a.OnClick = js.WebAuthnTriggersPasskeySettingsRegistration.String() + "()"
+ a.OnClickTrigger = js.WebAuthnTriggersPasskeySettingsRegistration
}),
).WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterPasskey()))
diff --git a/selfservice/strategy/passkey/passkey_settings_test.go b/selfservice/strategy/passkey/passkey_settings_test.go
index ced111071711..a37fe39a38f1 100644
--- a/selfservice/strategy/passkey/passkey_settings_test.go
+++ b/selfservice/strategy/passkey/passkey_settings_test.go
@@ -54,7 +54,7 @@ func TestCompleteSettings(t *testing.T) {
fix := newSettingsFixture(t)
fix.conf.MustSet(ctx, config.ViperKeyPasskeyRPID, "")
id := fix.createIdentity(t)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil)
require.NoError(t, err)
@@ -67,7 +67,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=a device is shown which can be unlinked", func(t *testing.T) {
id := fix.createIdentity(t)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -81,7 +81,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=invalid credentials", func(t *testing.T) {
id, _ := fix.createIdentityAndReturnIdentifier(t, []byte(`{invalid}`))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil)
require.NoError(t, err)
@@ -95,7 +95,7 @@ func TestCompleteSettings(t *testing.T) {
id := fix.createIdentityWithoutPasskey(t)
require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -110,7 +110,7 @@ func TestCompleteSettings(t *testing.T) {
id := fix.createIdentityWithoutPasskey(t)
require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS)
for _, n := range f.Ui.Nodes {
assert.NotEqual(t, n.Group, "passkey", "unexpected group: %s", n.Group)
@@ -118,7 +118,7 @@ func TestCompleteSettings(t *testing.T) {
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -127,7 +127,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -234,7 +234,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, &id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS)
// We inject the session to replay
@@ -271,6 +271,13 @@ func TestCompleteSettings(t *testing.T) {
flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData)))
testhelpers.EnsureAAL(t, browserClient, fix.publicTS, "aal1", string(identity.CredentialsTypePasskey))
+
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.uiTS.URL, "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
t.Run("type=browser", func(t *testing.T) {
@@ -431,7 +438,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, &id)
req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil)
require.NoError(t, err)
diff --git a/selfservice/strategy/passkey/testfixture_test.go b/selfservice/strategy/passkey/testfixture_test.go
index d7abf8459fb6..1f3090177341 100644
--- a/selfservice/strategy/passkey/testfixture_test.go
+++ b/selfservice/strategy/passkey/testfixture_test.go
@@ -207,8 +207,8 @@ func (fix *fixture) submitWebAuthnLoginWithClient(t *testing.T, isSPA bool, cont
return fix.submitWebAuthnLoginFlowWithClient(t, isSPA, f, contextFixture, client, cb)
}
-func (fix *fixture) submitWebAuthnLogin(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+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(t, ctx, fix.reg, id)
return fix.submitWebAuthnLoginWithClient(t, isSPA, contextFixture, browserClient, cb, opts...)
}
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..4b13f4012f6f
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "password",
+ "type": "password",
+ "required": true,
+ "autocomplete": "current-password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070001,
+ "text": "Password",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010022,
+ "text": "Sign in with password",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..eb9d0e213786
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1,67 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "value": "some@user.com",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "password",
+ "type": "password",
+ "required": true,
+ "autocomplete": "current-password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070001,
+ "text": "Password",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010001,
+ "text": "Sign in",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "password",
+ "type": "password",
+ "required": true,
+ "autocomplete": "current-password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070001,
+ "text": "Password",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010022,
+ "text": "Sign in with password",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "password",
+ "type": "password",
+ "required": true,
+ "autocomplete": "current-password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070001,
+ "text": "Password",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010022,
+ "text": "Sign in with password",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "password",
+ "type": "password",
+ "required": true,
+ "autocomplete": "current-password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070001,
+ "text": "Password",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010022,
+ "text": "Sign in with password",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "password",
+ "type": "password",
+ "required": true,
+ "autocomplete": "current-password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070001,
+ "text": "Password",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "password",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "password",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010022,
+ "text": "Sign in with password",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go
index 3600e29b4e0e..3b487a59057f 100644
--- a/selfservice/strategy/password/login.go
+++ b/selfservice/strategy/password/login.go
@@ -10,33 +10,31 @@ import (
"net/http"
"time"
- "github.com/ory/kratos/hash"
- "github.com/ory/kratos/selfservice/flowhelpers"
- "github.com/ory/kratos/selfservice/hook"
- "github.com/ory/kratos/session"
-
- "github.com/ory/x/stringsx"
-
"github.com/gofrs/uuid"
-
- "github.com/pkg/errors"
-
"github.com/ory/herodot"
- "github.com/ory/x/decoderx"
-
+ "github.com/ory/kratos/hash"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/selfservice/flowhelpers"
+ "github.com/ory/kratos/selfservice/hook"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+ "github.com/ory/kratos/session"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
+ "github.com/ory/x/decoderx"
+ "github.com/ory/x/stringsx"
+ "github.com/pkg/errors"
)
+var _ login.FormHydrator = new(Strategy)
+
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
}
-func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithPasswordMethod, err error) error {
+func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, payload *updateLoginFlowWithPasswordMethod, err error) error {
if f != nil {
f.UI.Nodes.ResetNodes("password")
f.UI.Nodes.SetValueAttribute("identifier", stringsx.Coalesce(payload.Identifier, payload.LegacyIdentifier))
@@ -62,19 +60,19 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
decoderx.HTTPDecoderSetValidatePayloads(true),
decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema),
decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
}
f.TransientPayload = p.TransientPayload
if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
}
identifier := stringsx.Coalesce(p.Identifier, p.LegacyIdentifier)
i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), identifier)
if err != nil {
time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation))
- return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
+ return nil, s.handleLoginError(r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
}
var o identity.CredentialsPassword
@@ -92,27 +90,27 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
migrationHook := hook.NewPasswordMigrationHook(s.d, pwHook.Config)
err = migrationHook.Execute(r.Context(), &hook.PasswordMigrationRequest{Identifier: identifier, Password: p.Password})
if err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
}
if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
}
} else {
if err := hash.Compare(r.Context(), []byte(p.Password), []byte(o.HashedPassword)); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
+ return nil, s.handleLoginError(r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
}
if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) {
if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
}
}
}
f.Active = s.ID()
if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error())))
+ return nil, s.handleLoginError(r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error())))
}
return i, nil
@@ -144,43 +142,87 @@ func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID
return s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, i)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error {
- // This strategy can only solve AAL1
- if requestedAAL > identity.AuthenticatorAssuranceLevel1 {
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error {
+ identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
+ if identifier == "" {
return nil
}
- if sr.IsForced() {
- // We only show this method on a refresh request if the user has indeed a password set.
- identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
- if identifier == "" {
- return nil
- }
+ // If we don't have a password set, do not show the password field.
+ count, err := s.CountActiveFirstFactorCredentials(id.Credentials)
+ if err != nil {
+ return err
+ } else if count == 0 {
+ return nil
+ }
- count, err := s.CountActiveFirstFactorCredentials(id.Credentials)
- if err != nil {
+ sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
+ sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
+ sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin()))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) addIdentifierNode(r *http.Request, sr *login.Flow) error {
+ ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
+ if err != nil {
+ return err
+ }
+
+ identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
+ if err != nil {
+ return err
+ }
+
+ sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
+ if err := s.addIdentifierNode(r, sr); err != nil {
+ return err
+ }
+
+ sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
+ sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword()))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error {
+ o := login.NewFormHydratorOptions(opts)
+
+ var count int
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we can perform identity credentials discovery and
+ // hide this credential if it should not be included.
+ if count, err = s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials); err != nil {
return err
- } else if count == 0 {
- return nil
}
+ }
+ if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
- } else {
- ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
- if err != nil {
- return err
- }
- identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
- if err != nil {
- return err
- }
- sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
+ sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
+ sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword()))
}
- sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
- sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin()))
+ if count == 0 {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ return nil
+}
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
return nil
}
diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go
index 8c2f2cb73245..55281a6bb393 100644
--- a/selfservice/strategy/password/login_test.go
+++ b/selfservice/strategy/password/login_test.go
@@ -11,27 +11,31 @@ import (
"fmt"
"io"
"net/http"
+ "net/http/httptest"
"net/url"
"strings"
"testing"
"time"
- "github.com/gobuffalo/httptest"
- "github.com/gofrs/uuid"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/ory/x/snapshotx"
"github.com/ory/kratos/driver"
+ "github.com/ory/kratos/internal/registrationhelpers"
+
+ "github.com/ory/kratos/selfservice/flow"
+
+ "github.com/gofrs/uuid"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hash"
"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/schema"
- "github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/text"
"github.com/ory/kratos/x"
@@ -40,15 +44,18 @@ import (
"github.com/ory/x/ioutilx"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/tidwall/gjson"
)
//go:embed stub/login.schema.json
var loginSchema []byte
-func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) {
+func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) *identity.Identity {
p, _ := reg.Hasher(ctx).Generate(context.Background(), []byte(password))
iId := x.NewUUID()
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &identity.Identity{
+ id := &identity.Identity{
ID: iId,
Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -67,7 +74,9 @@ func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing
IdentityID: iId,
},
},
- }))
+ }
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, id))
+ return id
}
func TestCompleteLogin(t *testing.T) {
@@ -510,7 +519,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("do not show password method if identity has no password set", func(t *testing.T) {
id := identity.NewIdentity("")
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
res, err := browserClient.Get(publicTS.URL + login.RouteInitBrowserFlow + "?refresh=true")
require.NoError(t, err)
@@ -570,7 +579,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("do not show password method if identity has no password set", func(t *testing.T) {
id := identity.NewIdentity("")
- hc := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ hc := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitBrowserFlow+"?refresh=true"))
require.NoError(t, err)
@@ -629,7 +638,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("do not show password method if identity has no password set", func(t *testing.T) {
id := identity.NewIdentity("")
- hc := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ hc := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitAPIFlow+"?refresh=true"))
require.NoError(t, err)
@@ -737,6 +746,32 @@ func TestCompleteLogin(t *testing.T) {
assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
})
+ t.Run("should succeed and include redirect continue_with in SPA flow", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ createIdentity(ctx, reg, t, identifier, pwd)
+
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false)
+ values := url.Values{"method": {"password"}, "identifier": {strings.ToUpper(identifier)}, "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode()
+ body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values)
+
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body)
+ })
+
+ t.Run("should succeed and not have redirect continue_with in api flow", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ createIdentity(ctx, reg, t, identifier, pwd)
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ body, res := testhelpers.LoginMakeRequest(t, true, true, f, browserClient, fmt.Sprintf(`{"method":"password","identifier":"%s","password":"%s"}`, strings.ToUpper(identifier), pwd))
+
+ assert.EqualValues(t, http.StatusOK, res.StatusCode, body)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ })
+
t.Run("should login even if old form field name is used", func(t *testing.T) {
identifier, pwd := x.NewUUID().String(), "password"
createIdentity(ctx, reg, t, identifier, pwd)
@@ -1064,3 +1099,113 @@ func TestCompleteLogin(t *testing.T) {
}
})
}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true})
+ ctx = testhelpers.WithDefaultIdentitySchemaFromRaw(ctx, loginSchema)
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePassword)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ id := createIdentity(ctx, reg, t, "some@user.com", "password")
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled and identity has no password", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has password", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ id := createIdentity(ctx, reg, t, identifier, pwd)
+
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a password", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go
index 14bf2382b212..d52ca2d77707 100644
--- a/selfservice/strategy/password/registration_test.go
+++ b/selfservice/strategy/password/registration_test.go
@@ -14,6 +14,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/ory/kratos/driver"
"github.com/ory/kratos/internal/registrationhelpers"
@@ -106,7 +108,7 @@ func TestRegistration(t *testing.T) {
})
})
- var expectLoginBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
+ var expectRegistrationBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
if isAPI {
return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values,
isSPA, http.StatusOK,
@@ -126,17 +128,17 @@ func TestRegistration(t *testing.T) {
isSPA, http.StatusOK, expectReturnTo)
}
- var expectSuccessfulLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
+ var expectSuccessfulRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
useReturnToFromTS(redirTS)
- return expectLoginBody(t, redirTS, isAPI, isSPA, hc, values)
+ return expectRegistrationBody(t, redirTS, isAPI, isSPA, hc, values)
}
- var expectNoLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
+ var expectNoRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
useReturnToFromTS(redirNoSessionTS)
t.Cleanup(func() {
useReturnToFromTS(redirTS)
})
- return expectLoginBody(t, redirNoSessionTS, isAPI, isSPA, hc, values)
+ return expectRegistrationBody(t, redirNoSessionTS, isAPI, isSPA, hc, values)
}
t.Run("case=should reject invalid transient payload", func(t *testing.T) {
@@ -178,7 +180,7 @@ func TestRegistration(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
username := x.NewUUID().String()
- body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) {
setValues(username, v)
})
assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body)
@@ -188,7 +190,7 @@ func TestRegistration(t *testing.T) {
t.Run("type=spa", func(t *testing.T) {
username := x.NewUUID().String()
- body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) {
setValues(username, v)
})
assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body)
@@ -198,7 +200,7 @@ func TestRegistration(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
username := x.NewUUID().String()
- body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
setValues(username, v)
})
assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body)
@@ -213,7 +215,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=api", func(t *testing.T) {
- body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-api")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -221,10 +223,11 @@ func TestRegistration(t *testing.T) {
assert.Equal(t, `registration-identifier-8-api`, gjson.Get(body, "identity.traits.username").String(), "%s", body)
assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body)
assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body)
+ assert.NotContains(t, gjson.Get(body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", body)
})
t.Run("type=spa", func(t *testing.T) {
- body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-spa")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -232,15 +235,17 @@ func TestRegistration(t *testing.T) {
assert.Equal(t, `registration-identifier-8-spa`, gjson.Get(body, "identity.traits.username").String(), "%s", body)
assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body)
assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
})
t.Run("type=browser", func(t *testing.T) {
- body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-browser")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
})
assert.Equal(t, `registration-identifier-8-browser`, gjson.Get(body, "identity.traits.username").String(), "%s", body)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
})
@@ -249,7 +254,7 @@ func TestRegistration(t *testing.T) {
conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
t.Run("type=api", func(t *testing.T) {
- body := expectNoLogin(t, true, false, nil, func(v url.Values) {
+ body := expectNoRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-api-nosession")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -260,7 +265,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- expectNoLogin(t, false, true, nil, func(v url.Values) {
+ expectNoRegistration(t, false, true, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-spa-nosession")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -268,7 +273,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- expectNoLogin(t, false, false, nil, func(v url.Values) {
+ expectNoRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-browser-nosession")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -300,7 +305,7 @@ func TestRegistration(t *testing.T) {
v.Set("traits.foobar", "bar")
}
- _ = expectSuccessfulLogin(t, true, false, apiClient, values)
+ _ = expectSuccessfulRegistration(t, true, false, apiClient, values)
body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS,
applyTransform(values, transform), false, http.StatusBadRequest,
publicTS.URL+registration.RouteSubmitFlow)
@@ -314,7 +319,7 @@ func TestRegistration(t *testing.T) {
v.Set("traits.foobar", "bar")
}
- _ = expectSuccessfulLogin(t, false, true, nil, values)
+ _ = expectSuccessfulRegistration(t, false, true, nil, values)
body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "spa", applyTransform(values, transform))
assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-spa-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body)
})
@@ -326,7 +331,7 @@ func TestRegistration(t *testing.T) {
v.Set("traits.foobar", "bar")
}
- _ = expectSuccessfulLogin(t, false, false, nil, values)
+ _ = expectSuccessfulRegistration(t, false, false, nil, values)
body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "browser", applyTransform(values, transform))
assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-browser-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body)
})
@@ -541,7 +546,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=api", func(t *testing.T) {
- actual := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) {
+ actual := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-10-api")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -550,7 +555,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-10-spa")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -559,7 +564,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-10-browser")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -620,7 +625,7 @@ func TestRegistration(t *testing.T) {
username := "registration-custom-schema"
t.Run("type=api", func(t *testing.T) {
- body := expectNoLogin(t, true, false, nil, func(v url.Values) {
+ body := expectNoRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", username+"-api")
v.Set("password", x.NewUUID().String())
v.Set("traits.baz", "bar")
@@ -631,7 +636,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- expectNoLogin(t, false, true, nil, func(v url.Values) {
+ expectNoRegistration(t, false, true, nil, func(v url.Values) {
v.Set("traits.username", username+"-spa")
v.Set("password", x.NewUUID().String())
v.Set("traits.baz", "bar")
@@ -639,7 +644,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- expectNoLogin(t, false, false, nil, func(v url.Values) {
+ expectNoRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", username+"-browser")
v.Set("password", x.NewUUID().String())
v.Set("traits.baz", "bar")
diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go
index a4ee7e6c7fa0..e5ea909856b0 100644
--- a/selfservice/strategy/password/settings_test.go
+++ b/selfservice/strategy/password/settings_test.go
@@ -13,6 +13,8 @@ import (
"strings"
"testing"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/ory/kratos/internal/settingshelpers"
"github.com/ory/kratos/text"
@@ -82,7 +84,7 @@ func TestSettings(t *testing.T) {
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true)
- _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg)
+ settingsUI := testhelpers.NewSettingsUIFlowEchoServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
_ = testhelpers.NewLoginUIWith401Response(t, conf)
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m")
@@ -94,10 +96,10 @@ func TestSettings(t *testing.T) {
publicTS, _ := testhelpers.NewKratosServer(t, reg)
- browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1)
- browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity2)
- apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity1)
- apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity2)
+ browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity1)
+ browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity2)
+ apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity1)
+ apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity2)
t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) {
c := testhelpers.NewDebugClient(t)
@@ -242,15 +244,20 @@ func TestSettings(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow)
check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow)
check(t, actual)
+ 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(), settingsUI.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
- check(t, testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String()))
+ actual := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String())
+ check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
@@ -340,9 +347,9 @@ func TestSettings(t *testing.T) {
bi := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
si := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
ai := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
- browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, bi)
- spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, si)
- apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, ai)
+ browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, bi)
+ spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, si)
+ apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, ai)
var check = func(t *testing.T, actual string, id *identity.Identity) {
assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual)
@@ -433,12 +440,12 @@ func TestSettings(t *testing.T) {
var initClients = func(isAPI, isSPA bool, id *identity.Identity) (client1, client2 *http.Client) {
if isAPI {
- client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
- client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
+ client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
return client1, client2
}
- client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
- client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
+ client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
return client1, client2
}
@@ -485,8 +492,8 @@ func TestSettings(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json")
id := newIdentityWithoutCredentials(testhelpers.RandomEmail())
- browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
- api := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
+ api := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
for _, f := range []string{"spa", "api", "browser"} {
t.Run("type="+f, func(t *testing.T) {
diff --git a/selfservice/strategy/password/strategy_disabled_test.go b/selfservice/strategy/password/strategy_disabled_test.go
index 6cf92147c3a4..e95e98c5913e 100644
--- a/selfservice/strategy/password/strategy_disabled_test.go
+++ b/selfservice/strategy/password/strategy_disabled_test.go
@@ -4,6 +4,7 @@
package password_test
import (
+ "context"
"io"
"net/http"
"net/url"
@@ -54,7 +55,7 @@ func TestDisabledEndpoint(t *testing.T) {
t.Run("case=should not settings when password method is disabled", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/login.schema.json")
- c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, context.Background(), reg)
t.Run("method=GET", func(t *testing.T) {
t.Skip("GET is currently not supported for this endpoint.")
diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api#01.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json
similarity index 100%
rename from selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api#01.json
rename to selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json
diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go
index 7d0c831711c3..fa5b1652a130 100644
--- a/selfservice/strategy/profile/strategy_test.go
+++ b/selfservice/strategy/profile/strategy_test.go
@@ -92,10 +92,10 @@ func TestStrategyTraits(t *testing.T) {
browserIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive}
apiIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive}
- browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1)
- browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity2)
- apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity1)
- apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity2)
+ browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity1)
+ browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity2)
+ apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity1)
+ apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity2)
t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) {
setUnprivileged(t)
@@ -210,7 +210,7 @@ func TestStrategyTraits(t *testing.T) {
run(t, apiIdentity1, pr, settings.RouteInitAPIFlow)
})
- t.Run("type=api", func(t *testing.T) {
+ t.Run("type=spa", func(t *testing.T) {
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendApi.CreateBrowserSettingsFlow(context.Background()).Execute()
require.NoError(t, err)
run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow)
@@ -449,15 +449,20 @@ func TestStrategyTraits(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
actual := expectSuccess(t, true, false, apiUser1, payload("not-john-doe-api@mail.com"))
check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=sqa", func(t *testing.T) {
actual := expectSuccess(t, false, true, browserUser1, payload("not-john-doe-browser@mail.com"))
check(t, actual)
+ 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(), ui.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
- check(t, expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com")))
+ actual := expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com"))
+ check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
@@ -612,7 +617,7 @@ func TestDisabledEndpoint(t *testing.T) {
publicTS, _ := testhelpers.NewKratosServer(t, reg)
browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
- browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1)
+ browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, context.Background(), reg, browserIdentity1)
t.Run("case=should not submit when profile method is disabled", func(t *testing.T) {
t.Run("method=GET", func(t *testing.T) {
diff --git a/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json
index afae3de49f05..23611d1c2255 100644
--- a/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json
+++ b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json
@@ -15,6 +15,7 @@
{
"attributes": {
"disabled": false,
+ "maxlength": 6,
"name": "totp_code",
"node_type": "input",
"required": true,
diff --git a/selfservice/strategy/totp/generator.go b/selfservice/strategy/totp/generator.go
index fe79d8991d0d..9846506f1671 100644
--- a/selfservice/strategy/totp/generator.go
+++ b/selfservice/strategy/totp/generator.go
@@ -25,6 +25,7 @@ import (
// So we need 160/8 = 20 key length. stdtotp.Generate uses the key
// length for reading from crypto.Rand.
const secretSize = 160 / 8
+const digits = otp.DigitsSix
func NewKey(ctx context.Context, accountName string, d interface {
config.Provider
@@ -33,7 +34,7 @@ func NewKey(ctx context.Context, accountName string, d interface {
Issuer: d.Config().TOTPIssuer(ctx),
AccountName: accountName,
SecretSize: secretSize,
- Digits: otp.DigitsSix,
+ Digits: digits,
Period: 30,
})
if err != nil {
diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go
index 2aaface8dc5c..7b4564cb165d 100644
--- a/selfservice/strategy/totp/login.go
+++ b/selfservice/strategy/totp/login.go
@@ -50,7 +50,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au
}
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(node.NewInputField("totp_code", "", node.TOTPGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoLoginTOTPLabel()))
+ sr.UI.SetNode(node.NewInputField("totp_code", "", node.TOTPGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithMaxLengthInputAttribute(int(digits))).WithMetaLabel(text.NewInfoLoginTOTPLabel()))
sr.UI.GetNodes().Append(node.NewInputField("method", s.ID(), node.TOTPGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginTOTP()))
return nil
diff --git a/selfservice/strategy/totp/login_test.go b/selfservice/strategy/totp/login_test.go
index 6456ea7cc599..7a48424ab412 100644
--- a/selfservice/strategy/totp/login_test.go
+++ b/selfservice/strategy/totp/login_test.go
@@ -13,6 +13,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/ory/x/assertx"
"github.com/gofrs/uuid"
@@ -107,7 +109,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=totp payload is set when identity has totp", func(t *testing.T) {
id, _, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
@@ -117,7 +119,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=totp payload is not set when identity has no totp", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
@@ -126,7 +128,7 @@ func TestCompleteLogin(t *testing.T) {
id, _, _ := createIdentity(t, reg)
t.Run("type=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ")
@@ -136,7 +138,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
body, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
@@ -146,7 +148,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
@@ -157,7 +159,7 @@ func TestCompleteLogin(t *testing.T) {
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "totp")
@@ -167,7 +169,7 @@ func TestCompleteLogin(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity, returnTo string) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
opts := []testhelpers.InitFlowWithOption{testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)}
if len(returnTo) > 0 {
@@ -333,23 +335,36 @@ func TestCompleteLogin(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
body, res := doAPIFlow(t, payload, id)
check(t, false, body, res)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload, id, "")
check(t, true, body, res)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=browser set return_to", func(t *testing.T) {
returnTo := "https://www.ory.sh"
- _, res := doBrowserFlow(t, false, payload, id, returnTo)
+ body, res := doBrowserFlow(t, false, payload, id, returnTo)
t.Log(res.Request.URL.String())
assert.Contains(t, res.Request.URL.String(), returnTo)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload, id, "")
check(t, false, body, res)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body)
+ })
+
+ t.Run("type=spa set return_to", func(t *testing.T) {
+ returnTo := "https://www.ory.sh"
+ body, res := doBrowserFlow(t, true, payload, id, returnTo)
+ check(t, false, body, res)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.EqualValues(t, returnTo, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body)
})
})
diff --git a/selfservice/strategy/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go
index 0fd479f1b220..b44cc736a560 100644
--- a/selfservice/strategy/totp/settings_test.go
+++ b/selfservice/strategy/totp/settings_test.go
@@ -64,7 +64,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=device unlinking is available when identity has totp", func(t *testing.T) {
id, _, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
@@ -76,7 +76,7 @@ func TestCompleteSettings(t *testing.T) {
id.Credentials = nil
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
@@ -87,7 +87,7 @@ func TestCompleteSettings(t *testing.T) {
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "totp")
@@ -97,7 +97,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "totp")
@@ -241,6 +241,7 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual)
checkIdentity(t, id)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
@@ -250,6 +251,9 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual)
checkIdentity(t, id)
+
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
@@ -259,6 +263,7 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual)
checkIdentity(t, id)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
@@ -344,12 +349,19 @@ func TestCompleteSettings(t *testing.T) {
checkIdentity(t, id, key)
testhelpers.EnsureAAL(t, hc, publicTS, "aal2", string(identity.CredentialsTypeTOTP))
+
+ if isSPA {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
+ } else {
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
+ }
}
t.Run("type=api", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
run(t, true, false, id, apiClient, f)
@@ -358,7 +370,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=spa", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, true, publicTS)
run(t, false, true, id, user, f)
@@ -367,7 +379,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, false, publicTS)
run(t, false, false, id, user, f)
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 ca960c98d683..b180cf04a403 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
@@ -24,25 +24,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -61,7 +42,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -70,5 +51,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
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 f4be195cdecf..399562e7015d 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-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"type": "text/javascript",
"node_type": "script"
},
@@ -51,6 +51,7 @@
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
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 f4be195cdecf..399562e7015d 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-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"type": "text/javascript",
"node_type": "script"
},
@@ -51,6 +51,7 @@
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json
index 6668b171ed43..052fc466dc5b 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json
@@ -46,7 +46,7 @@
"meta": {
"label": {
"id": 1010008,
- "text": "Use security key",
+ "text": "Sign in with hardware key",
"type": "info"
}
},
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
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-passwordless_enabled=false-case=mfa_v0_credentials-browser.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json
index 581bff275b17..5021f44d2f94 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-passwordless_enabled=false-case=mfa_v0_credentials-browser.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
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-passwordless_enabled=false-case=mfa_v0_credentials-spa.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json
index 581bff275b17..5021f44d2f94 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-passwordless_enabled=false-case=mfa_v0_credentials-spa.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
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-passwordless_enabled=false-case=mfa_v1_credentials-browser.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json
index 581bff275b17..5021f44d2f94 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-passwordless_enabled=false-case=mfa_v1_credentials-browser.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
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-passwordless_enabled=false-case=mfa_v1_credentials-spa.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json
index 581bff275b17..5021f44d2f94 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-passwordless_enabled=false-case=mfa_v1_credentials-spa.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json
new file mode 100644
index 000000000000..5021f44d2f94
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json
@@ -0,0 +1,75 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "identifier",
+ "node_type": "input",
+ "type": "hidden",
+ "value": "foo@bar.com"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login",
+ "node_type": "input",
+ "type": "hidden",
+ "value": ""
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "async": true,
+ "crossorigin": "anonymous",
+ "id": "webauthn_script",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "node_type": "script",
+ "referrerpolicy": "no-referrer",
+ "type": "text/javascript"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json
new file mode 100644
index 000000000000..5021f44d2f94
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json
@@ -0,0 +1,75 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "identifier",
+ "node_type": "input",
+ "type": "hidden",
+ "value": "foo@bar.com"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login",
+ "node_type": "input",
+ "type": "hidden",
+ "value": ""
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "async": true,
+ "crossorigin": "anonymous",
+ "id": "webauthn_script",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "node_type": "script",
+ "referrerpolicy": "no-referrer",
+ "type": "text/javascript"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
+ }
+]
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 0b1702c09413..f0edfe3c5966 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
@@ -82,33 +82,33 @@
{
"attributes": {
"disabled": false,
- "name": "webauthn_register_trigger",
+ "name": "webauthn_register",
"node_type": "input",
- "type": "button",
+ "type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
- "meta": {
- "label": {
- "id": 1050012,
- "text": "Add security key",
- "type": "info"
- }
- },
+ "meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
- "name": "webauthn_register",
+ "name": "webauthn_register_trigger",
"node_type": "input",
- "type": "hidden",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
- "meta": {},
+ "meta": {
+ "label": {
+ "id": 1050012,
+ "text": "Add security key",
+ "type": "info"
+ }
+ },
"type": "input"
},
{
@@ -116,7 +116,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
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 b21fa4833028..c15a847d4703 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
@@ -34,33 +34,33 @@
{
"attributes": {
"disabled": false,
- "name": "webauthn_register_trigger",
+ "name": "webauthn_register",
"node_type": "input",
- "type": "button",
+ "type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
- "meta": {
- "label": {
- "id": 1050012,
- "text": "Add security key",
- "type": "info"
- }
- },
+ "meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
- "name": "webauthn_register",
+ "name": "webauthn_register_trigger",
"node_type": "input",
- "type": "hidden",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
- "meta": {},
+ "meta": {
+ "label": {
+ "id": 1050012,
+ "text": "Add security key",
+ "type": "info"
+ }
+ },
"type": "input"
},
{
@@ -68,7 +68,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json
new file mode 100644
index 000000000000..ddd8316aa00f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "required": true,
+ "autocomplete": "username webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json
new file mode 100644
index 000000000000..1be62bb13f42
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login_trigger",
+ "type": "button",
+ "disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login",
+ "type": "hidden",
+ "value": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json
new file mode 100644
index 000000000000..1be62bb13f42
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login_trigger",
+ "type": "button",
+ "disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login",
+ "type": "hidden",
+ "value": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json
new file mode 100644
index 000000000000..1be62bb13f42
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login_trigger",
+ "type": "button",
+ "disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login",
+ "type": "hidden",
+ "value": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json
@@ -0,0 +1 @@
+[]
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 14a920d0a18d..20e3d3566fb0 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
@@ -75,8 +75,8 @@
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
- "type": "button",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
@@ -94,7 +94,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"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 14a920d0a18d..20e3d3566fb0 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
@@ -75,8 +75,8 @@
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
- "type": "button",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
@@ -94,7 +94,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go
index 4c7dd23f09ea..fe98d1d88c55 100644
--- a/selfservice/strategy/webauthn/login.go
+++ b/selfservice/strategy/webauthn/login.go
@@ -9,6 +9,8 @@ import (
"strings"
"time"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
"github.com/ory/kratos/selfservice/flowhelpers"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x/webauthnx"
@@ -34,84 +36,14 @@ import (
"github.com/ory/x/decoderx"
)
+var _ login.FormHydrator = new(Strategy)
+
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
webauthnx.RegisterWebauthnRoute(r)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error {
- if sr.Type != flow.TypeBrowser {
- return nil
- }
-
- if s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel1) {
- if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
- return nil
- } else if sr.IsForced() {
- if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
- return nil
- } else if !s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel2) {
- // We have done proper validation before so this should never error
- sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r)
- if err != nil {
- return err
- }
-
- if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
-
- return nil
- }
-
- return nil
-}
-
func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login.Flow) error {
- if sr.IsForced() {
- identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
- if identifier == "" {
- return nil
- }
-
- if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
-
- sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
- return nil
- }
-
- ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
- if err != nil {
- return err
- }
- identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
- if err != nil {
- return err
- }
-
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(node.NewInputField(
- "identifier",
- "",
- node.DefaultGroup,
- node.InputAttributeTypeText,
- node.WithRequiredInputAttribute,
- func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" },
- ).WithMetaLabel(identifierLabel))
sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn()))
return nil
}
@@ -133,11 +65,7 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident
return errors.WithStack(err)
}
- webAuthCreds := conf.Credentials.ToWebAuthn()
- if !sr.IsForced() {
- webAuthCreds = conf.Credentials.ToWebAuthnFiltered(aal)
- }
-
+ webAuthCreds := conf.Credentials.ToWebAuthnFiltered(aal)
if len(webAuthCreds) == 0 {
// Identity has no webauthn
return webauthnx.ErrNoCredentials
@@ -245,7 +173,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
return nil, s.handleLoginError(r, f, err)
}
- if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsForced() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 {
+ if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsRefresh() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 {
return s.loginPasswordless(w, r, f, &p)
}
@@ -337,7 +265,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f *
}
webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal)
- if f.IsForced() {
+ if f.IsRefresh() {
webAuthCreds = o.Credentials.ToWebAuthn()
}
@@ -365,3 +293,124 @@ func (s *Strategy) loginMultiFactor(w http.ResponseWriter, r *http.Request, f *l
}
return s.loginAuthenticate(w, r, f, identityID, p, identity.AuthenticatorAssuranceLevel2)
}
+
+func (s *Strategy) populateLoginMethodRefresh(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
+ if identifier == "" {
+ return nil
+ }
+
+ if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), sr.RequestedAAL); errors.Is(err, webauthnx.ErrNoCredentials) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return s.populateLoginMethodRefresh(r, sr)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return s.populateLoginMethodRefresh(r, sr)
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) {
+ return nil
+ }
+
+ ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
+ if err != nil {
+ return err
+ }
+
+ identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
+ if err != nil {
+ return err
+ }
+
+ sr.UI.SetNode(node.NewInputField(
+ "identifier",
+ "",
+ node.DefaultGroup,
+ node.InputAttributeTypeText,
+ node.WithRequiredInputAttribute,
+ func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" },
+ ).WithMetaLabel(identifierLabel))
+
+ if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser || s.d.Config().WebAuthnForPasswordless(r.Context()) {
+ return nil
+ }
+
+ // We have done proper validation before so this should never error
+ sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r)
+ if err != nil {
+ return err
+ }
+
+ if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error {
+ if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ o := login.NewFormHydratorOptions(opts)
+
+ var count int
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we can perform identity credentials discovery and
+ // hide this credential if it should not be included.
+ if count, err = s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials); err != nil {
+ return err
+ }
+ }
+
+ if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
+ if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+ return nil
+ } else if err != nil {
+ return err
+ }
+ }
+
+ if count == 0 {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
+ return nil
+}
diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go
index f5d332182163..ef7a40662dc4 100644
--- a/selfservice/strategy/webauthn/login_test.go
+++ b/selfservice/strategy/webauthn/login_test.go
@@ -10,8 +10,12 @@ import (
"fmt"
"io"
"net/http"
+ "net/http/httptest"
"net/url"
"testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
"github.com/ory/x/jsonx"
@@ -30,6 +34,7 @@ import (
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/internal"
"github.com/ory/kratos/internal/testhelpers"
@@ -153,7 +158,7 @@ func TestCompleteLogin(t *testing.T) {
}
submitWebAuthnLogin := func(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
return submitWebAuthnLoginWithClient(t, isSPA, id, contextFixture, browserClient, cb, opts...)
}
@@ -163,19 +168,30 @@ func TestCompleteLogin(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, nil)
})
- run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) {
+ run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel, expectTriggers bool) {
body, res, f := submitWebAuthnLogin(t, isSPA, id, context, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.WebAuthnLogin, string(response))
- }, testhelpers.InitFlowWithRefresh())
+ },
+ testhelpers.InitFlowWithRefresh(),
+ testhelpers.InitFlowWithAAL(expectedAAL),
+ )
snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
- "2.attributes.onclick",
- "4.attributes.nonce",
- "4.attributes.src",
+ "3.attributes.nonce",
+ "3.attributes.src",
+ "4.attributes.value",
+ "4.attributes.onclick",
})
+
nodes, err := json.Marshal(f.Ui.Nodes)
require.NoError(t, err)
+
+ if !expectTriggers {
+ assert.Falsef(t, gjson.GetBytes(nodes, "#(attributes.name==identifier)").Exists(), "%s", nodes)
+ return
+ }
+
assert.Equal(t, loginFixtureSuccessEmail, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").String(), "%s", nodes)
prefix := ""
@@ -208,40 +224,44 @@ func TestCompleteLogin(t *testing.T) {
}
for _, tc := range []struct {
- creds identity.Credentials
- response []byte
- context []byte
- descript string
+ creds identity.Credentials
+ response []byte
+ context []byte
+ descript string
+ expectTriggers bool
}{
{
creds: identity.Credentials{
Config: loginFixtureSuccessV0Credentials,
Version: 0,
},
- context: loginFixtureSuccessV0Context,
- response: loginFixtureSuccessV0Response,
- descript: "mfa v0 credentials",
+ context: loginFixtureSuccessV0Context,
+ response: loginFixtureSuccessV0Response,
+ descript: "mfa v0 credentials",
+ expectTriggers: !e,
},
{
creds: identity.Credentials{
Config: loginFixtureSuccessV1Credentials,
Version: 1,
},
- context: loginFixtureSuccessV1Context,
- response: loginFixtureSuccessV1Response,
- descript: "mfa v1 credentials",
+ context: loginFixtureSuccessV1Context,
+ response: loginFixtureSuccessV1Response,
+ descript: "mfa v1 credentials",
+ expectTriggers: !e,
},
{
creds: identity.Credentials{
Config: loginFixtureSuccessV1PasswordlessCredentials,
Version: 1,
},
- context: loginFixtureSuccessV1PasswordlessContext,
- response: loginFixtureSuccessV1PasswordlessResponse,
- descript: "passwordless credentials",
+ context: loginFixtureSuccessV1PasswordlessContext,
+ response: loginFixtureSuccessV1PasswordlessResponse,
+ descript: "passwordless credentials",
+ expectTriggers: e,
},
} {
- t.Run(fmt.Sprintf("case=mfa v0 credentials/passwordless enabled=%v", e), func(t *testing.T) {
+ t.Run(fmt.Sprintf("passwordless enabled=%v/case=%s", e, tc.descript), func(t *testing.T) {
id := createIdentityWithWebAuthn(t, tc.creds)
for _, f := range []string{
@@ -249,7 +269,7 @@ func TestCompleteLogin(t *testing.T) {
"spa",
} {
t.Run(f, func(t *testing.T) {
- run(t, id, tc.context, tc.response, f == "spa", expectedAAL)
+ run(t, id, tc.context, tc.response, f == "spa", expectedAAL, tc.expectTriggers)
})
}
})
@@ -264,7 +284,7 @@ func TestCompleteLogin(t *testing.T) {
for _, f := range []string{"browser", "spa"} {
t.Run(f, func(t *testing.T) {
id := identity.NewIdentity("")
- client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, true, f == "spa", false, false)
snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -317,7 +337,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=webauthn shows error if user tries to sign in but user has no webauth credentials set up", func(t *testing.T) {
- id, subject := createIdentityAndReturnIdentifier(t, reg, nil)
+ id, subject := createIdentityAndReturnIdentifier(t, ctx, reg, nil)
id.DeleteCredentialsType(identity.CredentialsTypeWebAuthn)
require.NoError(t, reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits))
@@ -344,7 +364,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=webauthn MFA credentials can not be used for passwordless login", func(t *testing.T) {
- _, subject := createIdentityAndReturnIdentifier(t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`))
+ _, subject := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`))
payload := func(v url.Values) {
v.Set("method", identity.CredentialsTypeWebAuthn.String())
@@ -369,7 +389,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) {
- _, subject := createIdentityAndReturnIdentifier(t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+ _, subject := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
doBrowserFlow := func(t *testing.T, spa bool, browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) {
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, spa, false, false, opts...)
@@ -446,6 +466,13 @@ func TestCompleteLogin(t *testing.T) {
actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)))
+
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
t.Run("type=browser", func(t *testing.T) {
@@ -460,25 +487,26 @@ func TestCompleteLogin(t *testing.T) {
t.Run("flow=mfa", func(t *testing.T) {
t.Run("case=webauthn payload is set when identity has webauthn", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assert.Equal(t, gjson.GetBytes(id.Traits, "subject").String(), f.Ui.Nodes[1].Attributes.UiNodeInputAttributes.Value, jsonx.TestMarshalJSONString(t, f.Ui))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
"1.attributes.value",
- "2.attributes.onclick",
- "2.attributes.onload",
- "4.attributes.src",
- "4.attributes.nonce",
+ "3.attributes.src",
+ "3.attributes.nonce",
+ "4.attributes.onclick",
+ "4.attributes.onload",
+ "4.attributes.value",
})
- ensureReplacement(t, "2", f.Ui, "allowCredentials")
+ ensureReplacement(t, "4", f.Ui, "allowCredentials")
})
t.Run("case=webauthn payload is not set when identity has no webauthn", func(t *testing.T) {
id := createIdentityWithoutWebAuthn(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -487,23 +515,23 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=webauthn payload is not set for API clients", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
doAPIFlowSignedIn := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- return doAPIFlow(t, v, testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
+ return doAPIFlow(t, v, testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
}
doBrowserFlowSignIn := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- return doBrowserFlow(t, spa, v, testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
+ return doBrowserFlow(t, spa, v, testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
}
t.Run("case=should refuse to execute api flow", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
payload := func(v url.Values) {
v.Set(node.WebAuthnLogin, "{}")
}
@@ -515,7 +543,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) {
- id, sub := createIdentityAndReturnIdentifier(t, reg, nil)
+ id, sub := createIdentityAndReturnIdentifier(t, ctx, reg, nil)
payload := func(v url.Values) {
v.Set("identifier", sub)
v.Set(node.WebAuthnLogin, string(loginFixtureSuccessResponseInvalid))
@@ -615,3 +643,242 @@ func TestCompleteLogin(t *testing.T) {
})
})
}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".enabled", true)
+ ctx = configtesthelpers.WithConfigValue(
+ ctx,
+ config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config",
+ map[string]interface{}{
+ "rp": map[string]interface{}{
+ "display_name": "foo",
+ "id": "localhost",
+ "origins": []string{"http://localhost"},
+ },
+ },
+ )
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/login.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeWebAuthn)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ f.UI.Nodes.ResetNodes("identifier")
+ f.UI.Nodes.ResetNodes("webauthn_login_trigger")
+ snapshotx.SnapshotT(t, f.UI.Nodes, snapshotx.ExceptNestedKeys("onclick", "nonce", "src"))
+ }
+
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ f.UI.Nodes = make(node.Nodes, 0)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ passwordlessEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true)
+ mfaEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, false)
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ id := createIdentity(t, ctx, reg)
+ headers := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+
+ r.Header = headers
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+
+ r.Header = headers
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodRefresh", func(t *testing.T) {
+ t.Run("case=passwordless enabled but user has no passwordless credentials", func(t *testing.T) {
+ id := createIdentity(t, ctx, reg)
+ r, f := newFlow(passwordlessEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=passwordless enabled and user has passwordless credentials", func(t *testing.T) {
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+ r, f := newFlow(passwordlessEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled and user has mfa credentials", func(t *testing.T) {
+ id := createIdentity(t, ctx, reg)
+ r, f := newFlow(mfaEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled but user has passwordless credentials", func(t *testing.T) {
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+ r, f := newFlow(mfaEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ mfaEnabled := configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ passwordlessEnabled := configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("test-provider")
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ mfaEnabled := configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ passwordlessEnabled := configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+
+ t.Run("case=identity has webauthn", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=identity does not have a webauthn", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(passwordlessEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+ })
+}
diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go
index 85cb3628e59d..e3ca6c9e5fd7 100644
--- a/selfservice/strategy/webauthn/registration.go
+++ b/selfservice/strategy/webauthn/registration.go
@@ -76,9 +76,10 @@ func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Reques
// we only set the value and not the whole field because we want to keep types from the initial form generation
f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
+
+ f.UI.Nodes.SetValueAttribute(node.WebAuthnRegisterDisplayName, p.RegisterDisplayName)
}
- 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/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go
index c0503b151ed8..8dd3e38bd036 100644
--- a/selfservice/strategy/webauthn/registration_test.go
+++ b/selfservice/strategy/webauthn/registration_test.go
@@ -145,6 +145,7 @@ func TestRegistration(t *testing.T) {
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"2.attributes.value",
"5.attributes.onclick",
+ "5.attributes.value",
"6.attributes.nonce",
"6.attributes.src",
})
@@ -367,6 +368,13 @@ func TestRegistration(t *testing.T) {
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)
+ }
})
}
})
diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go
index acf4fd357b1d..01ccdbf48578 100644
--- a/selfservice/strategy/webauthn/settings_test.go
+++ b/selfservice/strategy/webauthn/settings_test.go
@@ -53,19 +53,20 @@ var settingsFixtureSuccessInternalContext []byte
const registerDisplayNameGJSONQuery = "ui.nodes.#(attributes.name==" + node.WebAuthnRegisterDisplayName + ")"
func createIdentityWithoutWebAuthn(t *testing.T, reg driver.Registry) *identity.Identity {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
delete(id.Credentials, identity.CredentialsTypeWebAuthn)
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
return id
}
-func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf []byte) (*identity.Identity, string) {
+func createIdentityAndReturnIdentifier(t *testing.T, ctx context.Context, reg driver.Registry, conf []byte) (*identity.Identity, string) {
identifier := x.NewUUID().String() + "@ory.sh"
password := x.NewUUID().String()
- p, err := reg.Hasher(ctx).Generate(context.Background(), []byte(password))
+ p, err := reg.Hasher(ctx).Generate(ctx, []byte(password))
require.NoError(t, err)
i := &identity.Identity{
- Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
+ SchemaID: "default",
+ Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
VerifiableAddresses: []identity.VerifiableAddress{
{
Value: identifier,
@@ -77,7 +78,7 @@ func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf [
if conf == nil {
conf = []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo"},{"id":"YmFyYmFy","display_name":"bar"}]}`)
}
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
i.Credentials = map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {
Type: identity.CredentialsTypePassword,
@@ -90,12 +91,12 @@ func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf [
Config: conf,
},
}
- require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), i))
+ require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, i))
return i, identifier
}
-func createIdentity(t *testing.T, reg driver.Registry) *identity.Identity {
- id, _ := createIdentityAndReturnIdentifier(t, reg, nil)
+func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry) *identity.Identity {
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, nil)
return id
}
@@ -136,48 +137,50 @@ func TestCompleteSettings(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
t.Run("case=a device is shown which can be unlinked", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
- "4.attributes.onclick",
+ "5.attributes.onclick",
+ "5.attributes.value",
"6.attributes.src",
"6.attributes.nonce",
})
- ensureReplacement(t, "4", f.Ui, "Ory Corp")
+ ensureReplacement(t, "5", f.Ui, "Ory Corp")
})
t.Run("case=one activation element is shown", func(t *testing.T) {
id := createIdentityWithoutWebAuthn(t, reg)
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
- "2.attributes.onload",
- "2.attributes.onclick",
+ "3.attributes.onload",
+ "3.attributes.onclick",
+ "3.attributes.value",
"4.attributes.src",
"4.attributes.nonce",
})
- ensureReplacement(t, "2", f.Ui, "Ory Corp")
+ ensureReplacement(t, "3", f.Ui, "Ory Corp")
})
t.Run("case=webauthn only works for browsers", func(t *testing.T) {
id := createIdentityWithoutWebAuthn(t, reg)
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
assert.Empty(t, f.Ui.Nodes)
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -186,7 +189,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -314,7 +317,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
// We inject the session to replay
@@ -367,7 +370,7 @@ func TestCompleteSettings(t *testing.T) {
})
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
id.DeleteCredentialsType(identity.CredentialsTypePassword)
conf := sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)
id.UpsertCredentialsConfig(identity.CredentialsTypeWebAuthn, conf, 0)
@@ -375,7 +378,7 @@ func TestCompleteSettings(t *testing.T) {
body, res := doBrowserFlow(t, spa, func(v url.Values) {
// The remove key should be empty
- snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"})
+ snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"})
v.Set(node.WebAuthnRemove, "666f6f666f6f")
}, id)
@@ -409,14 +412,17 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=possible to remove webauthn credential if it is MFA at all times", func(t *testing.T) {
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
id.DeleteCredentialsType(identity.CredentialsTypePassword)
id.UpsertCredentialsConfig(identity.CredentialsTypeWebAuthn, sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":false}]}`), 0)
require.NoError(t, reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits))
body, res := doBrowserFlow(t, spa, func(v url.Values) {
// The remove key should be set
- snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"})
+ snapshotx.SnapshotTExcept(t, v, []string{
+ "csrf_token",
+ "webauthn_register_trigger",
+ })
v.Set(node.WebAuthnRemove, "666f6f666f6f")
}, id)
@@ -446,7 +452,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=remove all security keys", func(t *testing.T) {
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
allCred, ok := id.GetCredentials(identity.CredentialsTypeWebAuthn)
assert.True(t, ok)
@@ -465,6 +471,13 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
}
assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body)
+
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID)
@@ -487,7 +500,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=fails with browser submit register payload is invalid", func(t *testing.T) {
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
body, res := doBrowserFlow(t, spa, func(v url.Values) {
v.Set(node.WebAuthnRemove, fmt.Sprintf("%x", []byte("foofoo")))
}, id)
@@ -526,7 +539,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, isSPA, publicTS)
// We inject the session to replay
diff --git a/session/handler_test.go b/session/handler_test.go
index 3c61b7764832..c11a8f4a548e 100644
--- a/session/handler_test.go
+++ b/session/handler_test.go
@@ -560,7 +560,7 @@ func TestHandlerAdminSessionManagement(t *testing.T) {
})
t.Run("should redirect to public for whoami", func(t *testing.T) {
- client := testhelpers.NewHTTPClientWithSessionToken(t, reg, s)
+ client := testhelpers.NewHTTPClientWithSessionToken(t, ctx, reg, s)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
diff --git a/spec/api.json b/spec/api.json
index 1bb1345dc25c..e07a31dfc058 100644
--- a/spec/api.json
+++ b/spec/api.json
@@ -465,6 +465,7 @@
"continueWith": {
"discriminator": {
"mapping": {
+ "redirect_browser_to": "#/components/schemas/continueWithRedirectBrowserTo",
"set_ory_session_token": "#/components/schemas/continueWithSetOrySessionToken",
"show_recovery_ui": "#/components/schemas/continueWithRecoveryUi",
"show_settings_ui": "#/components/schemas/continueWithSettingsUi",
@@ -484,6 +485,9 @@
},
{
"$ref": "#/components/schemas/continueWithRecoveryUi"
+ },
+ {
+ "$ref": "#/components/schemas/continueWithRedirectBrowserTo"
}
]
},
@@ -516,7 +520,7 @@
"type": "string"
},
"url": {
- "description": "The URL of the recovery flow",
+ "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
"type": "string"
}
},
@@ -525,6 +529,28 @@
],
"type": "object"
},
+ "continueWithRedirectBrowserTo": {
+ "description": "Indicates, that the UI flow could be continued by showing a recovery ui",
+ "properties": {
+ "action": {
+ "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString",
+ "enum": [
+ "redirect_browser_to"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString"
+ },
+ "redirect_browser_to": {
+ "description": "The URL to redirect the browser to",
+ "type": "string"
+ }
+ },
+ "required": [
+ "action",
+ "redirect_browser_to"
+ ],
+ "type": "object"
+ },
"continueWithSetOrySessionToken": {
"description": "Indicates that a session was issued, and the application should use this token for authenticated requests",
"properties": {
@@ -574,6 +600,10 @@
"description": "The ID of the settings flow",
"format": "uuid",
"type": "string"
+ },
+ "url": {
+ "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
+ "type": "string"
}
},
"required": [
@@ -610,7 +640,7 @@
"type": "string"
},
"url": {
- "description": "The URL of the verification flow",
+ "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
"type": "string"
},
"verifiable_address": {
@@ -1080,10 +1110,6 @@
"hashed_password": {
"description": "HashedPassword is a hash-representation of the password.",
"type": "string"
- },
- "use_password_migration_hook": {
- "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.",
- "type": "boolean"
}
},
"title": "CredentialsPassword is contains the configuration for credentials of the type password.",
@@ -2173,7 +2199,7 @@
"$ref": "#/components/schemas/uiNodeAttributes"
},
"group": {
- "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup",
+ "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup",
"enum": [
"default",
"password",
@@ -2184,10 +2210,11 @@
"totp",
"lookup_secret",
"webauthn",
- "passkey"
+ "passkey",
+ "identifier_first"
],
"type": "string",
- "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup"
+ "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup"
},
"messages": {
"$ref": "#/components/schemas/uiTexts"
@@ -2349,6 +2376,11 @@
"label": {
"$ref": "#/components/schemas/uiText"
},
+ "maxlength": {
+ "description": "MaxLength may contain the input's maximum length.",
+ "format": "int64",
+ "type": "integer"
+ },
"name": {
"description": "The input's element name.",
"type": "string"
@@ -2366,13 +2398,39 @@
"x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script"
},
"onclick": {
- "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.",
+ "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.",
"type": "string"
},
+ "onclickTrigger": {
+ "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"onload": {
- "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.",
+ "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.",
"type": "string"
},
+ "onloadTrigger": {
+ "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"pattern": {
"description": "The input's pattern.",
"type": "string"
@@ -2601,6 +2659,7 @@
"discriminator": {
"mapping": {
"code": "#/components/schemas/updateLoginFlowWithCodeMethod",
+ "identifier_first": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod",
"lookup_secret": "#/components/schemas/updateLoginFlowWithLookupSecretMethod",
"oidc": "#/components/schemas/updateLoginFlowWithOidcMethod",
"passkey": "#/components/schemas/updateLoginFlowWithPasskeyMethod",
@@ -2631,6 +2690,9 @@
},
{
"$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod"
+ },
+ {
+ "$ref": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod"
}
]
},
@@ -2668,6 +2730,32 @@
],
"type": "object"
},
+ "updateLoginFlowWithIdentifierFirstMethod": {
+ "description": "Update Login Flow with Multi-Step Method",
+ "properties": {
+ "csrf_token": {
+ "description": "Sending the anti-csrf token is only required for browser login flows.",
+ "type": "string"
+ },
+ "identifier": {
+ "description": "Identifier is the email or username of the user trying to log in.",
+ "type": "string"
+ },
+ "method": {
+ "description": "Method should be set to \"password\" when logging in using the identifier and password strategy.",
+ "type": "string"
+ },
+ "transient_payload": {
+ "description": "Transient data to pass along to any webhooks",
+ "type": "object"
+ }
+ },
+ "required": [
+ "method",
+ "identifier"
+ ],
+ "type": "object"
+ },
"updateLoginFlowWithLookupSecretMethod": {
"description": "Update Login Flow with Lookup Secret Method",
"properties": {
@@ -2933,8 +3021,9 @@
"mapping": {
"code": "#/components/schemas/updateRegistrationFlowWithCodeMethod",
"oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod",
- "passKey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod",
+ "passkey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod",
"password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod",
+ "profile": "#/components/schemas/updateRegistrationFlowWithProfileMethod",
"webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod"
},
"propertyName": "method"
@@ -2954,6 +3043,9 @@
},
{
"$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ },
+ {
+ "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod"
}
]
},
@@ -4359,7 +4451,7 @@
},
"/admin/identities/{id}/credentials/{type}": {
"delete": {
- "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.",
+ "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.",
"operationId": "deleteIdentityCredentials",
"parameters": [
{
@@ -4372,7 +4464,7 @@
}
},
{
- "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
+ "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
"in": "path",
"name": "type",
"required": true,
@@ -4392,14 +4484,6 @@
"type": "string"
},
"x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode"
- },
- {
- "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.",
- "in": "query",
- "name": "identifier",
- "schema": {
- "type": "string"
- }
}
],
"responses": {
@@ -4981,7 +5065,7 @@
},
"/admin/sessions/{id}/extend": {
"patch": {
- "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
+ "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
"operationId": "extendSession",
"parameters": [
{
diff --git a/spec/swagger.json b/spec/swagger.json
index e790c71fecb4..83e4b809c022 100755
--- a/spec/swagger.json
+++ b/spec/swagger.json
@@ -662,7 +662,7 @@
"oryAccessToken": []
}
],
- "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.",
+ "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.",
"consumes": [
"application/json"
],
@@ -701,16 +701,10 @@
],
"type": "string",
"x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
- "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
+ "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
"name": "type",
"in": "path",
"required": true
- },
- {
- "type": "string",
- "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.",
- "name": "identifier",
- "in": "query"
}
],
"responses": {
@@ -1194,7 +1188,7 @@
"oryAccessToken": []
}
],
- "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
+ "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
"schemes": [
"http",
"https"
@@ -3654,7 +3648,29 @@
"format": "uuid"
},
"url": {
- "description": "The URL of the recovery flow",
+ "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
+ "type": "string"
+ }
+ }
+ },
+ "continueWithRedirectBrowserTo": {
+ "description": "Indicates, that the UI flow could be continued by showing a recovery ui",
+ "type": "object",
+ "required": [
+ "action",
+ "redirect_browser_to"
+ ],
+ "properties": {
+ "action": {
+ "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString",
+ "type": "string",
+ "enum": [
+ "redirect_browser_to"
+ ],
+ "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString"
+ },
+ "redirect_browser_to": {
+ "description": "The URL to redirect the browser to",
"type": "string"
}
}
@@ -3712,6 +3728,10 @@
"description": "The ID of the settings flow",
"type": "string",
"format": "uuid"
+ },
+ "url": {
+ "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
+ "type": "string"
}
}
},
@@ -3749,7 +3769,7 @@
"format": "uuid"
},
"url": {
- "description": "The URL of the verification flow",
+ "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
"type": "string"
},
"verifiable_address": {
@@ -4211,10 +4231,6 @@
"hashed_password": {
"description": "HashedPassword is a hash-representation of the password.",
"type": "string"
- },
- "use_password_migration_hook": {
- "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.",
- "type": "boolean"
}
}
},
@@ -5286,7 +5302,7 @@
"$ref": "#/definitions/uiNodeAttributes"
},
"group": {
- "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup",
+ "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup",
"type": "string",
"enum": [
"default",
@@ -5298,9 +5314,10 @@
"totp",
"lookup_secret",
"webauthn",
- "passkey"
+ "passkey",
+ "identifier_first"
],
- "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup"
+ "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup"
},
"messages": {
"$ref": "#/definitions/uiTexts"
@@ -5434,6 +5451,11 @@
"label": {
"$ref": "#/definitions/uiText"
},
+ "maxlength": {
+ "description": "MaxLength may contain the input's maximum length.",
+ "type": "integer",
+ "format": "int64"
+ },
"name": {
"description": "The input's element name.",
"type": "string"
@@ -5451,13 +5473,39 @@
"x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script"
},
"onclick": {
- "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.",
+ "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.",
"type": "string"
},
+ "onclickTrigger": {
+ "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "type": "string",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"onload": {
- "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.",
+ "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.",
"type": "string"
},
+ "onloadTrigger": {
+ "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "type": "string",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"pattern": {
"description": "The input's pattern.",
"type": "string"
@@ -5713,6 +5761,32 @@
}
}
},
+ "updateLoginFlowWithIdentifierFirstMethod": {
+ "description": "Update Login Flow with Multi-Step Method",
+ "type": "object",
+ "required": [
+ "method",
+ "identifier"
+ ],
+ "properties": {
+ "csrf_token": {
+ "description": "Sending the anti-csrf token is only required for browser login flows.",
+ "type": "string"
+ },
+ "identifier": {
+ "description": "Identifier is the email or username of the user trying to log in.",
+ "type": "string"
+ },
+ "method": {
+ "description": "Method should be set to \"password\" when logging in using the identifier and password strategy.",
+ "type": "string"
+ },
+ "transient_payload": {
+ "description": "Transient data to pass along to any webhooks",
+ "type": "object"
+ }
+ }
+ },
"updateLoginFlowWithLookupSecretMethod": {
"description": "Update Login Flow with Lookup Secret Method",
"type": "object",
diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts
index 477a149f9b26..3310a966627f 100644
--- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts
+++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts
@@ -83,7 +83,7 @@ context("Login error messages with code method", () => {
"An email containing a code has been sent to the email address you provided",
)
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/4010008"]').should(
@@ -113,7 +113,7 @@ context("Login error messages with code method", () => {
.type(gen.email(), { force: true })
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
if (app !== "express") {
@@ -147,7 +147,7 @@ context("Login error messages with code method", () => {
)
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.removeAttribute([Selectors[app]["identity"]], "required")
cy.get(Selectors[app]["identity"]).type("{selectall}{backspace}", {
diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts
index ef435991f736..4570c1c5cc60 100644
--- a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts
+++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts
@@ -74,7 +74,7 @@ context("Registration error messages with code method", () => {
"An email containing a code has been sent to the email address you provided",
)
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/4040003"]').should(
@@ -111,7 +111,7 @@ context("Registration error messages with code method", () => {
.type("changed-email@email.com", { force: true })
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
if (app !== "express") {
@@ -174,7 +174,7 @@ context("Registration error messages with code method", () => {
})
cy.removeAttribute([Selectors[app]["email"]], "required")
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
diff --git a/test/e2e/cypress/integration/profiles/mfa/code.spec.ts b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts
index 7961aa850de9..c7d29c0561c2 100644
--- a/test/e2e/cypress/integration/profiles/mfa/code.spec.ts
+++ b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts
@@ -58,7 +58,7 @@ context("2FA code", () => {
cy.get("input[name='code']").should("be.visible")
cy.getLoginCodeFromEmail(email).then((code) => {
cy.get("input[name='code']").type(code)
- cy.contains("Submit").click()
+ cy.contains("Continue").click()
})
cy.getSession({
@@ -88,10 +88,10 @@ context("2FA code", () => {
cy.get("input[name='code']").should("be.visible")
cy.get("input[name='code']").type("123456")
- cy.contains("Submit").click()
+ cy.contains("Continue").click()
cy.getLoginCodeFromEmail(email).then((code) => {
cy.get("input[name='code']").type(code)
- cy.contains("Submit").click()
+ cy.contains("Continue").click()
})
cy.getSession({
diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts
index 0b4584646abc..89b5c7cb15c5 100644
--- a/test/e2e/cypress/support/commands.ts
+++ b/test/e2e/cypress/support/commands.ts
@@ -17,7 +17,7 @@ import {
import dayjs from "dayjs"
import YAML from "yamljs"
import { MailMessage, Strategy } from "."
-import { OryKratosConfiguration } from "./config"
+import { OryKratosConfiguration } from "../../shared/config"
import { UiNode } from "@ory/kratos-client"
import { ConfigBuilder } from "./configHelpers"
@@ -429,7 +429,7 @@ Cypress.Commands.add(
f.group === "default" &&
"name" in f.attributes &&
f.attributes.name === "traits.email",
- ).attributes.value,
+ )?.attributes.value,
).to.eq(email)
return cy
diff --git a/test/e2e/cypress/support/configHelpers.ts b/test/e2e/cypress/support/configHelpers.ts
index c8ccf05b70d2..0fc72864294a 100644
--- a/test/e2e/cypress/support/configHelpers.ts
+++ b/test/e2e/cypress/support/configHelpers.ts
@@ -1,7 +1,7 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
-import { OryKratosConfiguration } from "./config"
+import { OryKratosConfiguration } from "../../shared/config"
export class ConfigBuilder {
constructor(readonly config: OryKratosConfiguration) {}
diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts
index 9cfeb083f12a..a6a7120937de 100644
--- a/test/e2e/cypress/support/index.d.ts
+++ b/test/e2e/cypress/support/index.d.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { Session as KratosSession } from "@ory/kratos-client"
-import { OryKratosConfiguration } from "./config"
+import { OryKratosConfiguration } from "../../shared/config"
import { ConfigBuilder } from "./configHelpers"
export interface MailMessage {
diff --git a/test/e2e/cypress/tsconfig.json b/test/e2e/cypress/tsconfig.json
index 6042605a6887..dd9b96adae48 100644
--- a/test/e2e/cypress/tsconfig.json
+++ b/test/e2e/cypress/tsconfig.json
@@ -2,15 +2,9 @@
"compilerOptions": {
"baseUrl": "../../../node_modules",
"target": "es5",
- "lib": [
- "es2015",
- "dom"
- ],
+ "lib": ["es2015", "dom"],
"types": ["cypress", "node"],
- "esModuleInterop": true,
+ "esModuleInterop": true
},
- "include": [
- "**/*.ts",
- "support/index.ts",
- ],
+ "include": ["**/*.ts", "support/index.ts", "../shared/config.d.ts"]
}
diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json
index f7f7bd88539a..f6452591bda9 100644
--- a/test/e2e/package-lock.json
+++ b/test/e2e/package-lock.json
@@ -8,13 +8,14 @@
"name": "@ory/kratos-e2e-suite",
"version": "0.0.1",
"dependencies": {
- "@faker-js/faker": "7.6.0",
+ "@faker-js/faker": "8.4.1",
"async-retry": "1.3.3",
- "mailhog": "4.16.0"
+ "mailhog": "4.16.0",
+ "promise-retry": "^2.0.1"
},
"devDependencies": {
- "@ory/kratos-client": "0.0.0-next.8d3b018594f7",
- "@playwright/test": "1.34.0",
+ "@ory/kratos-client": "1.2.0",
+ "@playwright/test": "1.44.1",
"@types/async-retry": "1.4.5",
"@types/node": "16.9.6",
"@types/yamljs": "0.2.31",
@@ -98,12 +99,19 @@
}
},
"node_modules/@faker-js/faker": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz",
- "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==",
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
+ "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
"engines": {
- "node": ">=14.0.0",
- "npm": ">=6.0.0"
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0",
+ "npm": ">=6.14.13"
}
},
"node_modules/@hapi/hoek": {
@@ -128,12 +136,13 @@
"dev": true
},
"node_modules/@ory/kratos-client": {
- "version": "0.0.0-next.8d3b018594f7",
- "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-0.0.0-next.8d3b018594f7.tgz",
- "integrity": "sha512-TkpjBo6Z6UUEJIJCR2EDdpKVDNgQHzwDWZbOjz3xTOUoGipMBykvIfluP58Jwkpt2rIXUkt9+L+u1mFFvD/tqA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.0.tgz",
+ "integrity": "sha512-W6jFkVEjnoq5ylGOvYOOaNvEZ1cGSEN/YJsZTcBVye81nQtW5R7QWClvNsJVD1LjwgWGMVKWglrFlfHvvkKnmg==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "axios": "^0.21.1"
+ "axios": "^1.6.1"
}
},
"node_modules/@otplib/core": {
@@ -184,22 +193,19 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.0.tgz",
- "integrity": "sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
+ "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "@types/node": "*",
- "playwright-core": "1.34.0"
+ "playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
- "node": ">=14"
- },
- "optionalDependencies": {
- "fsevents": "2.3.2"
+ "node": ">=16"
}
},
"node_modules/@sideway/address": {
@@ -534,14 +540,39 @@
"dev": true
},
"node_modules/axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "follow-redirects": "^1.14.0"
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
+ "node_modules/axios/node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1121,6 +1152,11 @@
"node": ">=8.6"
}
},
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="
+ },
"node_modules/es5-ext": {
"version": "0.10.62",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
@@ -1304,9 +1340,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
@@ -1314,6 +1350,7 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
+ "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -1373,6 +1410,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -2336,13 +2374,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/playwright": {
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
+ "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.44.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
"node_modules/playwright-core": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.0.tgz",
- "integrity": "sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
+ "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
"engines": {
- "node": ">=14"
+ "node": ">=16"
}
},
"node_modules/prettier": {
@@ -2381,6 +2442,26 @@
"node": ">= 0.6.0"
}
},
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/promise-retry/node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
@@ -3091,9 +3172,9 @@
}
},
"@faker-js/faker": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz",
- "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw=="
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
+ "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg=="
},
"@hapi/hoek": {
"version": "9.3.0",
@@ -3117,12 +3198,12 @@
"dev": true
},
"@ory/kratos-client": {
- "version": "0.0.0-next.8d3b018594f7",
- "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-0.0.0-next.8d3b018594f7.tgz",
- "integrity": "sha512-TkpjBo6Z6UUEJIJCR2EDdpKVDNgQHzwDWZbOjz3xTOUoGipMBykvIfluP58Jwkpt2rIXUkt9+L+u1mFFvD/tqA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.0.tgz",
+ "integrity": "sha512-W6jFkVEjnoq5ylGOvYOOaNvEZ1cGSEN/YJsZTcBVye81nQtW5R7QWClvNsJVD1LjwgWGMVKWglrFlfHvvkKnmg==",
"dev": true,
"requires": {
- "axios": "^0.21.1"
+ "axios": "^1.6.1"
}
},
"@otplib/core": {
@@ -3173,14 +3254,12 @@
}
},
"@playwright/test": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.0.tgz",
- "integrity": "sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
+ "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"requires": {
- "@types/node": "*",
- "fsevents": "2.3.2",
- "playwright-core": "1.34.0"
+ "playwright": "1.44.1"
}
},
"@sideway/address": {
@@ -3459,12 +3538,33 @@
"dev": true
},
"axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"dev": true,
"requires": {
- "follow-redirects": "^1.14.0"
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ }
}
},
"balanced-match": {
@@ -3914,6 +4014,11 @@
"ansi-colors": "^4.1.1"
}
},
+ "err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="
+ },
"es5-ext": {
"version": "0.10.62",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
@@ -4066,9 +4171,9 @@
}
},
"follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true
},
"forever-agent": {
@@ -4825,10 +4930,20 @@
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
+ "playwright": {
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
+ "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
+ "dev": true,
+ "requires": {
+ "fsevents": "2.3.2",
+ "playwright-core": "1.44.1"
+ }
+ },
"playwright-core": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.0.tgz",
- "integrity": "sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
+ "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true
},
"prettier": {
@@ -4849,6 +4964,22 @@
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true
},
+ "promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "requires": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "dependencies": {
+ "retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="
+ }
+ }
+ },
"proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
diff --git a/test/e2e/package.json b/test/e2e/package.json
index d4106cbb8aa1..05b04db6880a 100644
--- a/test/e2e/package.json
+++ b/test/e2e/package.json
@@ -11,13 +11,14 @@
"wait-on": "wait-on"
},
"dependencies": {
- "@faker-js/faker": "7.6.0",
+ "@faker-js/faker": "8.4.1",
"async-retry": "1.3.3",
- "mailhog": "4.16.0"
+ "mailhog": "4.16.0",
+ "promise-retry": "^2.0.1"
},
"devDependencies": {
- "@ory/kratos-client": "0.0.0-next.8d3b018594f7",
- "@playwright/test": "1.34.0",
+ "@ory/kratos-client": "1.2.0",
+ "@playwright/test": "1.44.1",
"@types/async-retry": "1.4.5",
"@types/node": "16.9.6",
"@types/yamljs": "0.2.31",
diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts
index 71a67dfd8795..a3f81c73060d 100644
--- a/test/e2e/playwright.config.ts
+++ b/test/e2e/playwright.config.ts
@@ -4,7 +4,7 @@
import { defineConfig, devices } from "@playwright/test"
import * as dotenv from "dotenv"
-dotenv.config({ path: "playwright/playwright.env" })
+dotenv.config({ path: __dirname + "/playwright/playwright.env" })
/**
* See https://playwright.dev/docs/test-configuration.
@@ -17,19 +17,28 @@ export default defineConfig({
workers: 1,
reporter: process.env.CI ? [["github"], ["html"], ["list"]] : "html",
- globalSetup: "./playwright/setup/global_setup.ts",
-
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
trace: process.env.CI ? "retain-on-failure" : "on",
- baseURL: "http://localhost:19006",
},
/* Configure projects for major browsers */
projects: [
{
- name: "Mobile Chrome",
- use: { ...devices["Pixel 5"] },
+ name: "mobile-chrome",
+ testMatch: "mobile/**/*.spec.ts",
+ use: {
+ ...devices["Pixel 5"],
+ baseURL: "http://localhost:19006",
+ },
+ },
+ {
+ name: "chromium",
+ testMatch: "desktop/**/*.spec.ts",
+ use: {
+ ...devices["Desktop Chrome"],
+ baseURL: "http://localhost:4455",
+ },
},
],
@@ -42,7 +51,6 @@ export default defineConfig({
].join(" && "),
cwd: "../..",
url: "http://localhost:4433/health/ready",
- reuseExistingServer: false,
env: {
DSN: dbToDsn(),
COURIER_SMTP_CONNECTION_URI:
diff --git a/test/e2e/playwright/actions/login.ts b/test/e2e/playwright/actions/login.ts
new file mode 100644
index 000000000000..806f16466dc3
--- /dev/null
+++ b/test/e2e/playwright/actions/login.ts
@@ -0,0 +1,47 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIRequestContext } from "@playwright/test"
+import { findCsrfToken } from "../lib/helper"
+import { LoginFlow, Session } from "@ory/kratos-client"
+import { expectJSONResponse } from "../lib/request"
+import { expect } from "../fixtures"
+
+export async function loginWithPassword(
+ user: { password: string; traits: { email: string } },
+ r: APIRequestContext,
+ baseUrl: string,
+): Promise {
+ const { ui } = await expectJSONResponse(
+ await r.get(baseUrl + "/self-service/login/browser", {
+ headers: {
+ Accept: "application/json",
+ },
+ }),
+ {
+ message: "Initializing login flow failed",
+ },
+ )
+
+ const res = await r.post(ui.action, {
+ headers: {
+ Accept: "application/json",
+ },
+ data: {
+ identifier: user.traits.email,
+ password: user.password,
+ method: "password",
+ csrf_token: findCsrfToken(ui),
+ },
+ })
+ const { session } = await expectJSONResponse<{ session: Session }>(res)
+ expect(session?.identity?.traits.email).toEqual(user.traits.email)
+ expect(
+ res.headersArray().find(
+ ({ name, value }) =>
+ name.toLowerCase() === "set-cookie" &&
+ (value.indexOf("ory_session_") > -1 || // Ory Network
+ value.indexOf("ory_kratos_session") > -1), // Locally hosted
+ ),
+ ).toBeDefined()
+}
diff --git a/test/e2e/playwright/actions/mail.ts b/test/e2e/playwright/actions/mail.ts
index 871608bc204d..172c6d8ab849 100644
--- a/test/e2e/playwright/actions/mail.ts
+++ b/test/e2e/playwright/actions/mail.ts
@@ -8,14 +8,29 @@ const mh = mailhog({
basePath: "http://localhost:8025/api",
})
-export function search(...props: Parameters) {
+type searchProps = {
+ query: string
+ kind: "to" | "from" | "containing"
+ /**
+ *
+ * @param message an email message
+ * @returns decide whether to include the message in the result
+ */
+ filter?: (message: mailhog.Message) => boolean
+}
+
+export function search({ query, kind, filter }: searchProps) {
return retry(
async () => {
- const res = await mh.search(...props)
+ const res = await mh.search(query, kind)
if (res.total === 0) {
throw new Error("no emails found")
}
- return res.items
+ const result = filter ? res.items.filter(filter) : res.items
+ if (result.length === 0) {
+ throw new Error("no emails found")
+ }
+ return result
},
{
retries: 3,
diff --git a/test/e2e/playwright/actions/session.ts b/test/e2e/playwright/actions/session.ts
new file mode 100644
index 000000000000..5d77b6de7b59
--- /dev/null
+++ b/test/e2e/playwright/actions/session.ts
@@ -0,0 +1,38 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIRequestContext, expect } from "@playwright/test"
+import { Session } from "@ory/kratos-client"
+
+export async function hasSession(
+ r: APIRequestContext,
+ kratosPublicURL: string,
+): Promise {
+ const resp = await r.get(kratosPublicURL + "/sessions/whoami", {
+ failOnStatusCode: true,
+ })
+ const session = await resp.json()
+ expect(session).toBeDefined()
+ expect(session.active).toBe(true)
+}
+
+export async function getSession(
+ r: APIRequestContext,
+ kratosPublicURL: string,
+): Promise {
+ const resp = await r.get(kratosPublicURL + "/sessions/whoami", {
+ failOnStatusCode: true,
+ })
+ return resp.json()
+}
+
+export async function hasNoSession(
+ r: APIRequestContext,
+ kratosPublicURL: string,
+): Promise {
+ const resp = await r.get(kratosPublicURL + "/sessions/whoami", {
+ failOnStatusCode: false,
+ })
+ expect(resp.status()).toBe(401)
+ return resp.json()
+}
diff --git a/test/e2e/playwright/fixtures/index.ts b/test/e2e/playwright/fixtures/index.ts
index 3b1264e84c0a..b915dd4d937f 100644
--- a/test/e2e/playwright/fixtures/index.ts
+++ b/test/e2e/playwright/fixtures/index.ts
@@ -1,13 +1,24 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
+import { faker } from "@faker-js/faker"
import { Identity } from "@ory/kratos-client"
-import { test as base, expect } from "@playwright/test"
-import { OryKratosConfiguration } from "../../cypress/support/config"
+import {
+ CDPSession,
+ test as base,
+ expect as baseExpect,
+ APIRequestContext,
+ Page,
+} from "@playwright/test"
+import { writeFile } from "fs/promises"
import { merge } from "lodash"
+import { OryKratosConfiguration } from "../../shared/config"
import { default_config } from "../setup/default_config"
-import { writeFile } from "fs/promises"
-import { faker } from "@faker-js/faker"
+import { APIResponse } from "playwright-core"
+import { SessionWithResponse } from "../types"
+import { retryOptions } from "../lib/request"
+import promiseRetry from "promise-retry"
+import { Protocol } from "playwright-core/types/protocol"
// from https://stackoverflow.com/questions/61132262/typescript-deep-partial
type DeepPartial = T extends object
@@ -17,12 +28,23 @@ type DeepPartial = T extends object
: T
type TestFixtures = {
- identity: Identity
+ identity: { oryIdentity: Identity; email: string; password: string }
configOverride: DeepPartial
- config: void
+ config: OryKratosConfiguration
+ virtualAuthenticatorOptions: Partial
+ pageCDPSession: CDPSession
+ virtualAuthenticator: Protocol.WebAuthn.addVirtualAuthenticatorReturnValue
}
-type WorkerFixtures = {}
+type WorkerFixtures = {
+ kratosAdminURL: string
+ kratosPublicURL: string
+ mode:
+ | "reconfigure_kratos"
+ | "reconfigure_ory_network_project"
+ | "existing_kratos"
+ | "existing_ory_network_project"
+}
export const test = base.extend({
configOverride: {},
@@ -34,9 +56,11 @@ export const test = base.extend({
const configRevision = await resp.body()
+ const fileDirectory = __dirname + "/../.."
+
await writeFile(
- "playwright/kratos.config.json",
- JSON.stringify(configToWrite),
+ fileDirectory + "/playwright/kratos.config.json",
+ JSON.stringify(configToWrite, null, 2),
)
await expect(async () => {
const resp = await request.get("http://localhost:4434/health/config")
@@ -44,21 +68,166 @@ export const test = base.extend({
expect(updatedRevision).not.toBe(configRevision)
}).toPass()
- await use()
+ await use(configToWrite)
},
{ auto: true },
],
- identity: async ({ request }, use) => {
+ virtualAuthenticatorOptions: undefined,
+ pageCDPSession: async ({ page }, use) => {
+ const cdpSession = await page.context().newCDPSession(page)
+ await use(cdpSession)
+ await cdpSession.detach()
+ },
+ virtualAuthenticator: async (
+ { pageCDPSession, virtualAuthenticatorOptions },
+ use,
+ ) => {
+ await pageCDPSession.send("WebAuthn.enable")
+ const { authenticatorId } = await pageCDPSession.send(
+ "WebAuthn.addVirtualAuthenticator",
+ {
+ options: {
+ protocol: "ctap2",
+ transport: "internal",
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ ...virtualAuthenticatorOptions,
+ },
+ },
+ )
+ await use({ authenticatorId })
+ await pageCDPSession.send("WebAuthn.removeVirtualAuthenticator", {
+ authenticatorId,
+ })
+
+ await pageCDPSession.send("WebAuthn.disable")
+ },
+ identity: async ({ request }, use, i) => {
+ const email = faker.internet.email({ provider: "ory.sh" })
+ const password = faker.internet.password()
const resp = await request.post("http://localhost:4434/admin/identities", {
data: {
schema_id: "email",
traits: {
- email: faker.internet.email(undefined, undefined, "ory.sh"),
+ email,
website: faker.internet.url(),
},
+
+ credentials: {
+ password: {
+ config: {
+ password,
+ },
+ },
+ },
},
})
+ const oryIdentity = await resp.json()
+ i.attach("identity", {
+ body: JSON.stringify(oryIdentity, null, 2),
+ contentType: "application/json",
+ })
expect(resp.status()).toBe(201)
- await use(await resp.json())
+ await use({
+ oryIdentity,
+ email,
+ password,
+ })
},
+ kratosAdminURL: ["http://localhost:4434", { option: true, scope: "worker" }],
+ kratosPublicURL: ["http://localhost:4433", { option: true, scope: "worker" }],
+})
+
+export const expect = baseExpect.extend({
+ toHaveSession,
+ toMatchResponseData,
})
+
+async function toHaveSession(
+ requestOrPage: APIRequestContext | Page,
+ baseUrl: string,
+) {
+ let r: APIRequestContext
+ if ("request" in requestOrPage) {
+ r = requestOrPage.request
+ } else {
+ r = requestOrPage
+ }
+ let pass = true
+
+ let responseData: string
+ let response: APIResponse = null
+ try {
+ const result = await promiseRetry(
+ () =>
+ r
+ .get(baseUrl + "/sessions/whoami", {
+ failOnStatusCode: false,
+ })
+ .then(
+ async (res: APIResponse): Promise => {
+ return {
+ session: await res.json(),
+ response: res,
+ }
+ },
+ ),
+ retryOptions,
+ )
+ pass = !!result.session.active
+ responseData = await result.response.text()
+ response = result.response
+ } catch (e) {
+ pass = false
+ responseData = JSON.stringify(e.message, undefined, 2)
+ }
+
+ const message = () =>
+ this.utils.matcherHint("toHaveSession", undefined, undefined, {
+ isNot: this.isNot,
+ }) +
+ `\n
+ \n
+ Expected: ${this.isNot ? "not" : ""} to have session\n
+ Session data received: ${responseData}\n
+ Headers: ${JSON.stringify(response?.headers(), null, 2)}\n
+ `
+
+ return {
+ message,
+ pass,
+ name: "toHaveSession",
+ }
+}
+
+async function toMatchResponseData(
+ res: APIResponse,
+ options: {
+ statusCode?: number
+ failureHint?: string
+ },
+) {
+ const body = await res.text()
+ const statusCode = options.statusCode ?? 200
+ const failureHint = options.failureHint ?? ""
+ const message = () =>
+ this.utils.matcherHint("toMatch", undefined, undefined, {
+ isNot: this.isNot,
+ }) +
+ `\n
+ ${failureHint}
+ \n
+ Expected: ${this.isNot ? "not" : ""} to match\n
+ Status Code: ${statusCode}\n
+ Body: ${body}\n
+ Headers: ${JSON.stringify(res.headers(), null, 2)}\n
+ URL: ${JSON.stringify(res.url(), null, 2)}\n
+ `
+
+ return {
+ message,
+ pass: res.status() === statusCode,
+ name: "toMatch",
+ }
+}
diff --git a/test/e2e/playwright/kratos.base-config.json b/test/e2e/playwright/kratos.base-config.json
index 83b24587bd61..e9abcfd7f4f0 100644
--- a/test/e2e/playwright/kratos.base-config.json
+++ b/test/e2e/playwright/kratos.base-config.json
@@ -4,6 +4,10 @@
{
"id": "default",
"url": "file://test/e2e/profiles/oidc/identity.traits.schema.json"
+ },
+ {
+ "id": "email",
+ "url": "file://test/e2e/profiles/email/identity.traits.schema.json"
}
]
},
diff --git a/test/e2e/playwright/lib/helper.ts b/test/e2e/playwright/lib/helper.ts
index 929b39b74573..da5fb76c13ba 100644
--- a/test/e2e/playwright/lib/helper.ts
+++ b/test/e2e/playwright/lib/helper.ts
@@ -2,6 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
import { Message } from "mailhog"
+import {
+ UiContainer,
+ UiNodeAttributes,
+ UiNodeInputAttributes,
+} from "@ory/kratos-client"
+import { expect } from "../fixtures"
+import { LoginFlowStyle, OryKratosConfiguration } from "../../shared/config"
export const codeRegex = /(\d{6})/
@@ -18,3 +25,50 @@ export function extractCode(mail: Message) {
}
return null
}
+
+export function findCsrfToken(ui: UiContainer) {
+ const csrf = ui.nodes
+ .filter((node) => isUiNodeInputAttributes(node.attributes))
+ // Since we filter all non-input attributes, the following as is ok:
+ .map(
+ (node): UiNodeInputAttributes => node.attributes as UiNodeInputAttributes,
+ )
+ .find(({ name }) => name === "csrf_token")?.value
+ expect(csrf).toBeDefined()
+ return csrf
+}
+
+export function isUiNodeInputAttributes(
+ attrs: UiNodeAttributes,
+): attrs is UiNodeInputAttributes & {
+ node_type: "input"
+} {
+ return attrs.node_type === "input"
+}
+
+export const toConfig = ({
+ style = "identifier_first",
+ mitigateEnumeration = false,
+ selfservice,
+}: {
+ style?: LoginFlowStyle
+ mitigateEnumeration?: boolean
+ selfservice?: Partial
+}) => ({
+ selfservice: {
+ default_browser_return_url: "http://localhost:4455/welcome",
+ ...selfservice,
+ flows: {
+ login: {
+ ...selfservice?.flows?.login,
+ style,
+ },
+ ...selfservice?.flows,
+ },
+ },
+ security: {
+ account_enumeration: {
+ mitigate: mitigateEnumeration,
+ },
+ },
+})
diff --git a/test/e2e/playwright/lib/request.ts b/test/e2e/playwright/lib/request.ts
new file mode 100644
index 000000000000..5732e5a34aca
--- /dev/null
+++ b/test/e2e/playwright/lib/request.ts
@@ -0,0 +1,34 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIResponse } from "playwright-core"
+import { expect } from "../fixtures"
+import { OperationOptions } from "retry"
+
+export type RetryOptions = OperationOptions
+
+export const retryOptions: RetryOptions = {
+ retries: 20,
+ factor: 1,
+ maxTimeout: 500,
+ minTimeout: 250,
+ randomize: false,
+}
+
+export async function expectJSONResponse(
+ res: APIResponse,
+ { statusCode = 200, message }: { statusCode?: number; message?: string } = {},
+): Promise {
+ await expect(res).toMatchResponseData({
+ statusCode,
+ failureHint: message,
+ })
+ try {
+ return (await res.json()) as T
+ } catch (e) {
+ const body = await res.text()
+ throw Error(
+ `Expected to be able to parse body as json: ${e} (body: ${body})`,
+ )
+ }
+}
diff --git a/test/e2e/playwright/models/elements/login.ts b/test/e2e/playwright/models/elements/login.ts
new file mode 100644
index 000000000000..6fcfad6ad6b8
--- /dev/null
+++ b/test/e2e/playwright/models/elements/login.ts
@@ -0,0 +1,246 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect, Locator, Page } from "@playwright/test"
+import { createInputLocator, InputLocator } from "../../selectors/input"
+import { URLSearchParams } from "node:url"
+import { OryKratosConfiguration } from "../../../shared/config"
+
+enum LoginStyle {
+ IdentifierFirst = "identifier_first",
+ OneStep = "one_step",
+}
+
+type SubmitOptions = {
+ submitWithKeyboard?: boolean
+ waitForURL?: string | RegExp
+}
+
+export class LoginPage {
+ public submitPassword: Locator
+ public github: Locator
+ public google: Locator
+ public signup: Locator
+
+ public identifier: InputLocator
+ public password: InputLocator
+ public totpInput: InputLocator
+ public totpSubmit: Locator
+ public lookupInput: InputLocator
+ public lookupSubmit: Locator
+ public codeSubmit = this.page.locator('button[type="submit"][value="code"]')
+ public codeInput = createInputLocator(this.page, "code")
+
+ public alert: Locator
+
+ constructor(readonly page: Page, readonly config: OryKratosConfiguration) {
+ this.identifier = createInputLocator(page, "identifier")
+ this.password = createInputLocator(page, "password")
+ this.totpInput = createInputLocator(page, "totp_code")
+ this.lookupInput = createInputLocator(page, "lookup_secret")
+
+ this.submitPassword = page.locator(
+ '[type="submit"][name="method"][value="password"]',
+ )
+
+ this.github = page.locator('[name="provider"][value="github"]')
+ this.google = page.locator('[name="provider"][value="google"]')
+
+ this.totpSubmit = page.locator('[name="method"][value="totp"]')
+ this.lookupSubmit = page.locator('[name="method"][value="lookup_secret"]')
+
+ this.signup = page.locator('[data-testid="signup-link"]')
+
+ // this.submitHydra = page.locator('[name="provider"][value="hydra"]')
+ // this.forgotPasswordLink = page.locator(
+ // "[data-testid='forgot-password-link']",
+ // )
+ // this.logoutLink = page.locator("[data-testid='logout-link']")
+ }
+
+ async submitIdentifierFirst(identifier: string) {
+ await this.inputField("identifier").fill(identifier)
+ await this.submit("identifier_first", {
+ waitForURL: new RegExp(this.config.selfservice.flows.login.ui_url),
+ })
+ }
+
+ async loginWithPassword(
+ identifier: string,
+ password: string,
+ opts?: SubmitOptions,
+ ) {
+ switch (this.config.selfservice.flows.login.style) {
+ case LoginStyle.IdentifierFirst:
+ await this.submitIdentifierFirst(identifier)
+ break
+ case LoginStyle.OneStep:
+ await this.inputField("identifier").fill(identifier)
+ break
+ }
+
+ await this.inputField("password").fill(password)
+ await this.submit("password", opts)
+ }
+
+ async triggerLoginWithCode(identifier: string, opts?: SubmitOptions) {
+ switch (this.config.selfservice.flows.login.style) {
+ case LoginStyle.IdentifierFirst:
+ await this.submitIdentifierFirst(identifier)
+ break
+ case LoginStyle.OneStep:
+ await this.inputField("identifier").fill(identifier)
+ break
+ }
+
+ await this.codeSubmit.click()
+ }
+
+ async open({
+ aal,
+ refresh,
+ }: {
+ aal?: string
+ refresh?: boolean
+ } = {}) {
+ const p = new URLSearchParams()
+ if (refresh) {
+ p.append("refresh", "true")
+ }
+
+ if (aal) {
+ p.append("aal", aal)
+ }
+
+ await Promise.all([
+ this.page.goto(
+ this.config.selfservice.flows.login.ui_url + "?" + p.toString(),
+ ),
+ this.isReady(),
+ this.page.waitForURL((url) =>
+ url.toString().includes(this.config.selfservice.flows.login.ui_url),
+ ),
+ ])
+ await this.isReady()
+ }
+
+ async isReady() {
+ await expect(this.inputField("csrf_token").nth(0)).toBeHidden()
+ }
+
+ submitMethod(method: string) {
+ switch (method) {
+ case "google":
+ case "github":
+ case "hydra":
+ return this.page.locator(`[name="provider"][value="${method}"]`)
+ }
+ return this.page.locator(`[name="method"][value="${method}"]`)
+ }
+
+ inputField(name: string) {
+ return this.page.locator(`input[name=${name}]`)
+ }
+
+ async submit(method: string, opts?: SubmitOptions) {
+ const nav = opts?.waitForURL
+ ? this.page.waitForURL(opts.waitForURL)
+ : Promise.resolve()
+ if (opts?.submitWithKeyboard) {
+ await this.page.keyboard.press("Enter")
+ } else {
+ await this.submitMethod(method).click()
+ }
+
+ await nav
+ }
+
+ //
+ // async submitPasswordForm(
+ // id: string,
+ // password: string,
+ // expectURL: string | RegExp,
+ // options: {
+ // submitWithKeyboard?: boolean
+ // style?: LoginStyle
+ // } = {
+ // submitWithKeyboard: false,
+ // style: LoginStyle.OneStep,
+ // },
+ // ) {
+ // await this.isReady()
+ // await this.inputField("identifier").fill(id)
+ //
+ // if (options.style === LoginStyle.IdentifierFirst) {
+ // await this.submitMethod("identifier_first").click()
+ // await this.inputField("password").fill(password)
+ // } else {
+ // await this.inputField("password").fill(password)
+ // }
+ //
+ // const nav = this.page.waitForURL(expectURL)
+ //
+ // if (submitWithKeyboard) {
+ // await this.page.keyboard.press("Enter")
+ // } else {
+ // await this.submitPassword.click()
+ // }
+ //
+ // await nav
+ // }
+ //
+ // readonly baseURL: string
+ // readonly submitHydra: Locator
+ // readonly forgotPasswordLink: Locator
+ // readonly logoutLink: Locator
+ //
+ // async goto(returnTo?: string, refresh?: boolean) {
+ // const u = new URL(routes.hosted.login(this.baseURL))
+ // if (returnTo) {
+ // u.searchParams.append("return_to", returnTo)
+ // }
+ // if (refresh) {
+ // u.searchParams.append("refresh", refresh.toString())
+ // }
+ // await this.page.goto(u.toString())
+ // await this.isReady()
+ // }
+ //
+ // async loginWithHydra(email: string, password: string) {
+ // await this.submitHydra.click()
+ // await this.page.waitForURL(new RegExp(OIDC_PROVIDER))
+ //
+ // await this.page.locator("input[name=email]").fill(email)
+ // await this.page.locator("input[name=password]").fill(password)
+ //
+ // await this.page.locator("input[name=submit][id=accept]").click()
+ // }
+ //
+ // async loginWithOIDC(email = generateEmail(), password = generatePassword()) {
+ // await this.page.fill('[name="email"]', email)
+ // await this.page.fill('[name="password"]', password)
+ // await this.page.click("#accept")
+ // }
+ //
+ // async loginAndAcceptConsent(
+ // email = generateEmail(),
+ // password = generatePassword(),
+ // {rememberConsent = true, rememberLogin = false} = {},
+ // ) {
+ // await this.page.fill('[name="email"]', email)
+ // await this.page.fill('[name="password"]', password)
+ // rememberLogin && (await this.page.check('[name="remember"]'))
+ // await this.page.click("#accept")
+ //
+ // await this.page.click("#offline")
+ // await this.page.click("#openid")
+ // rememberConsent && (await this.page.check("[name=remember]"))
+ // await this.page.click("#accept")
+ //
+ // return email
+ // }
+ //
+ // async expectAlert(id: string) {
+ // await this.page.getByTestId(`ui/message/${id}`).waitFor()
+ // }
+}
diff --git a/test/e2e/playwright/selectors/input.ts b/test/e2e/playwright/selectors/input.ts
new file mode 100644
index 000000000000..c4f1f35d0271
--- /dev/null
+++ b/test/e2e/playwright/selectors/input.ts
@@ -0,0 +1,19 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { Locator, Page } from "@playwright/test"
+
+export interface InputLocator {
+ input: Locator
+ message: Locator
+ label: Locator
+}
+
+export const createInputLocator = (page: Page, field: string): InputLocator => {
+ const prefix = `[data-testid="node/input/${field}"]`
+ return {
+ input: page.locator(`${prefix} input`),
+ label: page.locator(`${prefix} label`),
+ message: page.locator(`${prefix} p`),
+ }
+}
diff --git a/test/e2e/playwright/setup/default_config.ts b/test/e2e/playwright/setup/default_config.ts
index b9249917b039..5e4f5d1e2e73 100644
--- a/test/e2e/playwright/setup/default_config.ts
+++ b/test/e2e/playwright/setup/default_config.ts
@@ -1,7 +1,7 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
-import { OryKratosConfiguration } from "../../cypress/support/config"
+import { OryKratosConfiguration } from "../../shared/config"
export const default_config: OryKratosConfiguration = {
dsn: "",
@@ -11,6 +11,10 @@ export const default_config: OryKratosConfiguration = {
id: "default",
url: "file://test/e2e/profiles/oidc/identity.traits.schema.json",
},
+ {
+ id: "email",
+ url: "file://test/e2e/profiles/email/identity.traits.schema.json",
+ },
],
},
serve: {
@@ -40,7 +44,7 @@ export const default_config: OryKratosConfiguration = {
cipher: ["secret-thirty-two-character-long"],
},
selfservice: {
- default_browser_return_url: "http://localhost:4455/",
+ default_browser_return_url: "http://localhost:4455/welcome",
allowed_return_urls: [
"http://localhost:4455",
"http://localhost:19006",
diff --git a/test/e2e/playwright/setup/global_setup.ts b/test/e2e/playwright/setup/global_setup.ts
deleted file mode 100644
index 92a42432fc96..000000000000
--- a/test/e2e/playwright/setup/global_setup.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright © 2023 Ory Corp
-// SPDX-License-Identifier: Apache-2.0
-
-import fs from "fs"
-import { default_config } from "./default_config"
-
-export default async function globalSetup() {
- await fs.promises.writeFile(
- "playwright/kratos.config.json",
- JSON.stringify(default_config),
- )
-}
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts
new file mode 100644
index 000000000000..b94dbf29669c
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts
@@ -0,0 +1,224 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect } from "@playwright/test"
+import { search } from "../../../actions/mail"
+import { getSession, hasNoSession, hasSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { extractCode, toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+
+test.describe.parallel("account enumeration protection off", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: false,
+ selfservice: {
+ methods: {
+ code: {
+ passwordless_enabled: true,
+ },
+ },
+ },
+ }),
+ })
+
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('[data-testid="ui/message/4000037"]'),
+ "expect account not exist message to be shown",
+ ).toBeVisible()
+ })
+
+ test("login with wrong code fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ await login.codeInput.input.fill("123123")
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4010008"]'),
+ "expect to be shown a wrong code error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ const mails = await search({ query: identity.email, kind: "to" })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+})
+
+test.describe("account enumeration protection on", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: true,
+ selfservice: {
+ methods: {
+ code: {
+ passwordless_enabled: true,
+ },
+ },
+ },
+ }),
+ })
+
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('button[name="method"][value="code"]'),
+ "expect to show the code form",
+ ).toBeVisible()
+ })
+
+ test("login with wrong code fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ await login.codeInput.input.fill("123123")
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4010008"]'),
+ "expect to be shown a wrong code error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ const mails = await search({ query: identity.email, kind: "to" })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+})
+
+test.describe(() => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: false,
+ selfservice: {
+ methods: {
+ code: {
+ passwordless_enabled: true,
+ },
+ },
+ },
+ }),
+ })
+ test("refresh", async ({ page, identity, config, kratosPublicURL }) => {
+ const login = new LoginPage(page, config)
+
+ const [initialSession, initialCode] =
+ await test.step("initial login", async () => {
+ await login.open()
+ await login.triggerLoginWithCode(identity.email)
+
+ const mails = await search({ query: identity.email, kind: "to" })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ const session = await getSession(page.request, kratosPublicURL)
+ expect(session).toBeDefined()
+ expect(session.active).toBe(true)
+ return [session, code]
+ })
+
+ await login.open({
+ refresh: true,
+ })
+ await login.inputField("identifier").fill(identity.email)
+ await login.submit("code")
+
+ const mails = await search({
+ query: identity.email,
+ kind: "to",
+ filter: (m) => !m.html.includes(initialCode),
+ })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ const newSession = await getSession(page.request, kratosPublicURL)
+ expect(newSession).toBeDefined()
+ expect(newSession.active).toBe(true)
+
+ expect(initialSession.authenticated_at).not.toEqual(
+ newSession.authenticated_at,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts
new file mode 100644
index 000000000000..f14dbaebff02
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts
@@ -0,0 +1,157 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { faker } from "@faker-js/faker"
+import { expect, Page } from "@playwright/test"
+import { getSession, hasSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+import { OryKratosConfiguration } from "../../../../shared/config"
+
+async function loginHydra(page: Page) {
+ return test.step("login with hydra", async () => {
+ await page
+ .locator("input[name=username]")
+ .fill(faker.internet.email({ provider: "ory.sh" }))
+ await page.locator("button[name=action][value=accept]").click()
+ await page.locator("#offline").check()
+ await page.locator("#openid").check()
+
+ await page.locator("input[name=website]").fill(faker.internet.url())
+
+ await page.locator("button[name=action][value=accept]").click()
+ })
+}
+
+async function registerWithHydra(
+ page: Page,
+ config: OryKratosConfiguration,
+ kratosPublicURL: string,
+) {
+ return await test.step("register", async () => {
+ await page.goto("/registration")
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ const email = faker.internet.email({ provider: "ory.sh" })
+ await page.locator("input[name=username]").fill(email)
+ await page.locator("#remember").check()
+ await page.locator("button[name=action][value=accept]").click()
+ await page.locator("#offline").check()
+ await page.locator("#openid").check()
+
+ await page.locator("input[name=website]").fill(faker.internet.url())
+
+ await page.locator("button[name=action][value=accept]").click()
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ await page.context().clearCookies({
+ domain: new URL(kratosPublicURL).hostname,
+ })
+
+ await expect(
+ getSession(page.request, kratosPublicURL),
+ ).rejects.toThrowError()
+ return email
+ })
+}
+
+for (const mitigateEnumeration of [true, false]) {
+ test.describe(`account enumeration protection ${
+ mitigateEnumeration ? "on" : "off"
+ }`, () => {
+ test.use({
+ configOverride: toConfig({ mitigateEnumeration }),
+ })
+
+ test("login", async ({ page, config, kratosPublicURL }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+
+ test("oidc sign in on second step", async ({
+ page,
+ config,
+ kratosPublicURL,
+ }) => {
+ const email = await registerWithHydra(page, config, kratosPublicURL)
+
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst(email)
+
+ // If account enumeration is mitigated, we should see the password method,
+ // because the identity has not set up a password
+ await expect(
+ page.locator('button[name="method"][value="password"]'),
+ "hide the password method",
+ ).toBeVisible({ visible: mitigateEnumeration })
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ const session = await getSession(page.request, kratosPublicURL)
+ expect(session).toBeDefined()
+ expect(session.active).toBe(true)
+ })
+ })
+}
+
+test("login with refresh", async ({ page, config, kratosPublicURL }) => {
+ await registerWithHydra(page, config, kratosPublicURL)
+
+ const login = new LoginPage(page, config)
+
+ const initialSession = await test.step("initial login", async () => {
+ await login.open()
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ return await getSession(page.request, kratosPublicURL)
+ })
+
+ await test.step("refresh login", async () => {
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ const newSession = await getSession(page.request, kratosPublicURL)
+ expect(initialSession.authenticated_at).not.toEqual(
+ newSession.authenticated_at,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts
new file mode 100644
index 000000000000..36ee107df318
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts
@@ -0,0 +1,233 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { faker } from "@faker-js/faker"
+import { CDPSession, expect, Page } from "@playwright/test"
+import { OryKratosConfiguration } from "../../../../shared/config"
+import { getSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+
+async function toggleAutomaticPresenceSimulation(
+ cdpSession: CDPSession,
+ authenticatorId: string,
+ enabled: boolean,
+) {
+ await cdpSession.send("WebAuthn.setAutomaticPresenceSimulation", {
+ authenticatorId,
+ enabled,
+ })
+}
+
+async function registerWithPasskey(
+ page: Page,
+ pageCDPSession: CDPSession,
+ config: OryKratosConfiguration,
+ authenticatorId: string,
+ simulatePresence: boolean,
+) {
+ return await test.step("create webauthn identity", async () => {
+ await page.goto("/registration")
+ const identifier = faker.internet.email()
+ await page.locator(`input[name="traits.email"]`).fill(identifier)
+ await page
+ .locator(`input[name="traits.website"]`)
+ .fill(faker.internet.url())
+ await page.locator("button[name=method][value=profile]").click()
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ authenticatorId,
+ true,
+ )
+ await page.locator("button[name=passkey_register_trigger]").click()
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ authenticatorId,
+ simulatePresence,
+ )
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ return identifier
+ })
+}
+
+const passkeyConfig = {
+ methods: {
+ passkey: {
+ enabled: true,
+ config: {
+ rp: {
+ display_name: "ORY",
+ id: "localhost",
+ origins: ["http://localhost:4455"],
+ },
+ },
+ },
+ },
+}
+
+for (const mitigateEnumeration of [true, false]) {
+ test.describe(`account enumeration protection ${
+ mitigateEnumeration ? "on" : "off"
+ }`, () => {
+ test.use({
+ configOverride: toConfig({
+ mitigateEnumeration,
+ style: "identifier_first",
+ selfservice: passkeyConfig,
+ }),
+ })
+
+ for (const simulatePresence of [true, false]) {
+ test.describe(`${
+ simulatePresence ? "with" : "without"
+ } automatic presence proof`, () => {
+ test.use({
+ virtualAuthenticatorOptions: {
+ automaticPresenceSimulation: simulatePresence,
+ // hasResidentKey: simulatePresence,
+ },
+ })
+ test("login", async ({
+ config,
+ page,
+ kratosPublicURL,
+ virtualAuthenticator,
+ pageCDPSession,
+ }) => {
+ const identifier = await registerWithPasskey(
+ page,
+ pageCDPSession,
+ config,
+ virtualAuthenticator.authenticatorId,
+ simulatePresence,
+ )
+ await page.context().clearCookies({})
+
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ if (!simulatePresence) {
+ await login.submitIdentifierFirst(identifier)
+
+ const passkeyLoginTrigger = page.locator(
+ "button[name=passkey_login_trigger]",
+ )
+ await passkeyLoginTrigger.waitFor()
+
+ await page.waitForLoadState("load")
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ virtualAuthenticator.authenticatorId,
+ true,
+ )
+
+ await passkeyLoginTrigger.click()
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ virtualAuthenticator.authenticatorId,
+ false,
+ )
+ }
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ await expect(
+ getSession(page.request, kratosPublicURL),
+ ).resolves.toMatchObject({
+ active: true,
+ identity: {
+ traits: {
+ email: identifier,
+ },
+ },
+ })
+ })
+ })
+ }
+ })
+}
+
+test.describe("without automatic presence simulation", () => {
+ test.use({
+ virtualAuthenticatorOptions: {
+ automaticPresenceSimulation: false,
+ },
+ configOverride: toConfig({
+ selfservice: passkeyConfig,
+ }),
+ })
+ test("login with refresh", async ({
+ page,
+ config,
+ kratosPublicURL,
+ pageCDPSession,
+ virtualAuthenticator,
+ }) => {
+ const identifier = await registerWithPasskey(
+ page,
+ pageCDPSession,
+ config,
+ virtualAuthenticator.authenticatorId,
+ true,
+ )
+
+ const login = new LoginPage(page, config)
+ // Due to resetting automatic presence simulating to "true" in the previous step,
+ // opening the login page automatically triggers the passkey login
+ await login.open()
+
+ await expect(
+ getSession(page.request, kratosPublicURL),
+ ).resolves.toMatchObject({
+ active: true,
+ identity: {
+ traits: {
+ email: identifier,
+ },
+ },
+ })
+
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ const originalSession = await getSession(page.request, kratosPublicURL)
+
+ const passkeyLoginTrigger = page.locator(
+ "button[name=passkey_login_trigger]",
+ )
+ await passkeyLoginTrigger.waitFor()
+
+ await page.waitForLoadState("load")
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ virtualAuthenticator.authenticatorId,
+ true,
+ )
+
+ await passkeyLoginTrigger.click()
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ const newSession = await getSession(page.request, kratosPublicURL)
+ expect(originalSession.authenticated_at).not.toEqual(
+ newSession.authenticated_at,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts
new file mode 100644
index 000000000000..2891dbb0e885
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts
@@ -0,0 +1,214 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect } from "@playwright/test"
+import { loginWithPassword } from "../../../actions/login"
+import { getSession, hasNoSession, hasSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+
+// These can run in parallel because they use the same config.
+test.describe("account enumeration protection off", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: false,
+ }),
+ })
+
+ test.describe.configure({ mode: "parallel" })
+
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('[data-testid="ui/message/4000037"]'),
+ "expect account not exist message to be shown",
+ ).toBeVisible()
+ })
+
+ test("login with wrong password fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.loginWithPassword(identity.email, "wrong-password")
+ await login.isReady()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4000006"]'),
+ "expect to be shown a credentials do not exist error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.inputField("identifier").fill(identity.email)
+ await login.submit("identifier_first", {
+ waitForURL: new RegExp(config.selfservice.flows.login.ui_url),
+ })
+
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+
+ test("login with refresh", async ({
+ page,
+ config,
+ identity,
+ kratosPublicURL,
+ }) => {
+ await loginWithPassword(
+ {
+ password: identity.password,
+ traits: {
+ email: identity.email,
+ },
+ },
+ page.request,
+ kratosPublicURL,
+ )
+
+ const login = new LoginPage(page, config)
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ const originalSession = await getSession(page.request, kratosPublicURL)
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+ const newSession = await getSession(page.request, kratosPublicURL)
+ expect(originalSession.authenticated_at).not.toEqual(
+ newSession.authenticated_at,
+ )
+ })
+})
+
+test.describe("account enumeration protection on", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: true,
+ }),
+ })
+
+ test.describe.configure({ mode: "parallel" })
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('button[name="method"][value="password"]'),
+ "expect to show the password form",
+ ).toBeVisible()
+ })
+
+ test("login with wrong password fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.loginWithPassword(identity.email, "wrong-password")
+ await login.isReady()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4000006"]'),
+ "expect to be shown a credentials do not exist error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ // projectFrontendClient,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.inputField("identifier").fill(identity.email)
+ await login.submit("identifier_first", {
+ waitForURL: new RegExp(config.selfservice.flows.login.ui_url),
+ })
+
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+
+ test("login with refresh", async ({
+ page,
+ config,
+ identity,
+ kratosPublicURL,
+ }) => {
+ await loginWithPassword(
+ {
+ password: identity.password,
+ traits: {
+ email: identity.email,
+ },
+ },
+ page.request,
+ kratosPublicURL,
+ )
+
+ const login = new LoginPage(page, config)
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ const originalSession = await getSession(page.request, kratosPublicURL)
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+ const newSession = await getSession(page.request, kratosPublicURL)
+ expect(originalSession.authenticated_at).not.toEqual(
+ newSession.authenticated_at,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/app_login.spec.ts b/test/e2e/playwright/tests/mobile/app_login.spec.ts
similarity index 97%
rename from test/e2e/playwright/tests/app_login.spec.ts
rename to test/e2e/playwright/tests/mobile/app_login.spec.ts
index 600a49a48ce1..327fbbf3e54a 100644
--- a/test/e2e/playwright/tests/app_login.spec.ts
+++ b/test/e2e/playwright/tests/mobile/app_login.spec.ts
@@ -1,7 +1,8 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+import { test } from "../../fixtures"
test.describe.configure({ mode: "parallel" })
diff --git a/test/e2e/playwright/tests/app_recovery.spec.ts b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts
similarity index 78%
rename from test/e2e/playwright/tests/app_recovery.spec.ts
rename to test/e2e/playwright/tests/mobile/app_recovery.spec.ts
index 629abd3c05bc..c065e0965443 100644
--- a/test/e2e/playwright/tests/app_recovery.spec.ts
+++ b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
import { expect } from "@playwright/test"
-import { test } from "../fixtures"
-import { search } from "../actions/mail"
-import { extractCode } from "../lib/helper"
+import { test } from "../../fixtures"
+import { search } from "../../actions/mail"
+import { extractCode } from "../../lib/helper"
const schemaConfig = {
default_schema_id: "email",
@@ -31,11 +31,11 @@ test.describe("Recovery", () => {
test("succeeds with a valid email address", async ({ page, identity }) => {
await page.goto("/Recovery")
- await page.getByTestId("email").fill(identity.traits.email)
+ await page.getByTestId("email").fill(identity.email)
await page.getByTestId("submit-form").click()
await expect(page.getByTestId("ui/message/1060003")).toBeVisible()
- const mails = await search(identity.traits.email, "to")
+ const mails = await search({ query: identity.email, kind: "to" })
expect(mails).toHaveLength(1)
const code = extractCode(mails[0])
@@ -43,13 +43,13 @@ test.describe("Recovery", () => {
await test.step("enter wrong code", async () => {
await page.getByTestId("code").fill(wrongCode)
- await page.getByText("Submit").click()
+ await page.getByText("Continue").click()
await expect(page.getByTestId("ui/message/4060006")).toBeVisible()
})
await test.step("enter correct code", async () => {
await page.getByTestId("code").fill(code)
- await page.getByText("Submit").click()
+ await page.getByText("Continue").click()
await page.waitForURL(/Settings/)
await expect(page.getByTestId("ui/message/1060001").first()).toBeVisible()
})
@@ -58,13 +58,13 @@ test.describe("Recovery", () => {
test("wrong email address does not get sent", async ({ page, identity }) => {
await page.goto("/Recovery")
- const wrongEmailAddress = "wrong-" + identity.traits.email
+ const wrongEmailAddress = "wrong-" + identity.email
await page.getByTestId("email").fill(wrongEmailAddress)
await page.getByTestId("submit-form").click()
await expect(page.getByTestId("ui/message/1060003")).toBeVisible()
try {
- await search(identity.traits.email, "to")
+ await search({ query: identity.email, kind: "to" })
expect(false).toBeTruthy()
} catch (e) {
// this is expected
@@ -74,11 +74,11 @@ test.describe("Recovery", () => {
test("fails with an invalid code", async ({ page, identity }) => {
await page.goto("/Recovery")
- await page.getByTestId("email").fill(identity.traits.email)
+ await page.getByTestId("email").fill(identity.email)
await page.getByTestId("submit-form").click()
await page.getByTestId("ui/message/1060003").isVisible()
- const mails = await search(identity.traits.email, "to")
+ const mails = await search({ query: identity.email, kind: "to" })
expect(mails).toHaveLength(1)
const code = extractCode(mails[0])
@@ -87,14 +87,14 @@ test.describe("Recovery", () => {
await test.step("enter wrong repeatedly", async () => {
for (let i = 0; i < 10; i++) {
await page.getByTestId("code").fill(wrongCode)
- await page.getByText("Submit", { exact: true }).click()
+ await page.getByText("Continue", { exact: true }).click()
await expect(page.getByTestId("ui/message/4060006")).toBeVisible()
}
})
await test.step("enter correct code fails", async () => {
await page.getByTestId("code").fill(code)
- await page.getByText("Submit", { exact: true }).click()
+ await page.getByText("Continue", { exact: true }).click()
await expect(page.getByTestId("ui/message/4060006")).toBeVisible()
})
})
@@ -123,17 +123,17 @@ test.describe("Recovery", () => {
test("fails with an expired code", async ({ page, identity }) => {
await page.goto("/Recovery")
- await page.getByTestId("email").fill(identity.traits.email)
+ await page.getByTestId("email").fill(identity.email)
await page.getByTestId("submit-form").click()
await page.getByTestId("ui/message/1060003").isVisible()
- const mails = await search(identity.traits.email, "to")
+ const mails = await search({ query: identity.email, kind: "to" })
expect(mails).toHaveLength(1)
const code = extractCode(mails[0])
await page.getByTestId("code").fill(code)
- await page.getByText("Submit", { exact: true }).click()
+ await page.getByText("Continue", { exact: true }).click()
await expect(page.getByTestId("email")).toBeVisible()
})
})
diff --git a/test/e2e/playwright/types/index.ts b/test/e2e/playwright/types/index.ts
new file mode 100644
index 000000000000..ddb3431774c0
--- /dev/null
+++ b/test/e2e/playwright/types/index.ts
@@ -0,0 +1,10 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIResponse } from "playwright-core"
+import { Session } from "@ory/kratos-client"
+
+export type SessionWithResponse = {
+ session: Session
+ response: APIResponse
+}
diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml
index 3e98857e1628..a98e4979e730 100644
--- a/test/e2e/profiles/code/.kratos.yml
+++ b/test/e2e/profiles/code/.kratos.yml
@@ -38,7 +38,6 @@ selfservice:
enabled: true
code:
passwordless_enabled: true
- passwordless_login_fallback_enabled: false
enabled: true
config:
lifespan: 1h
diff --git a/test/e2e/profiles/mobile/.kratos.yml b/test/e2e/profiles/mobile/.kratos.yml
index c0a46e57c197..ab927737337d 100644
--- a/test/e2e/profiles/mobile/.kratos.yml
+++ b/test/e2e/profiles/mobile/.kratos.yml
@@ -19,7 +19,6 @@ selfservice:
verification:
enabled: false
-
methods:
totp:
enabled: true
diff --git a/test/e2e/profiles/two-steps/.kratos.yml b/test/e2e/profiles/two-steps/.kratos.yml
index d23dd0bce07c..f5271792c0fc 100644
--- a/test/e2e/profiles/two-steps/.kratos.yml
+++ b/test/e2e/profiles/two-steps/.kratos.yml
@@ -66,7 +66,6 @@ selfservice:
code:
enabled: true
passwordless_enabled: true
- passwordless_login_fallback_enabled: false
config:
lifespan: 1h
diff --git a/test/e2e/render-kratos-config.sh b/test/e2e/render-kratos-config.sh
index 5867904216e2..55a60965e280 100755
--- a/test/e2e/render-kratos-config.sh
+++ b/test/e2e/render-kratos-config.sh
@@ -10,7 +10,7 @@ ory_x_version="$(cd $dir/../..; go list -f '{{.Version}}' -m github.com/ory/x)"
curl -s https://raw.githubusercontent.com/ory/x/$ory_x_version/otelx/config.schema.json > $dir/.tracing-config.schema.json
-(cd $dir; sed "s!ory://tracing-config!.tracing-config.schema.json!g;" $dir/../../embedx/config.schema.json | npx json2ts --strictIndexSignatures > $dir/cypress/support/config.d.ts)
+(cd $dir; sed "s!ory://tracing-config!.tracing-config.schema.json!g;" $dir/../../embedx/config.schema.json | npx json2ts --strictIndexSignatures > $dir/shared/config.d.ts)
rm $dir/.tracing-config.schema.json
diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/shared/config.d.ts
similarity index 90%
rename from test/e2e/cypress/support/config.d.ts
rename to test/e2e/shared/config.d.ts
index c7e9742aed39..f423ba19855d 100644
--- a/test/e2e/cypress/support/config.d.ts
+++ b/test/e2e/shared/config.d.ts
@@ -53,10 +53,18 @@ export type ProvideLoginHintsOnFailedRegistration = boolean
* URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).
*/
export type RegistrationUIURL = string
+/**
+ * Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To revert to one-step registration, set this to `true`.
+ */
+export type DisableTwoStepRegistration = boolean
/**
* URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).
*/
export type LoginUIURL = string
+/**
+ * The style of the login flow. If set to `one_step` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.
+ */
+export type LoginFlowStyle = "one_step" | "identifier_first"
/**
* If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.sh/kratos/docs/self-service/flows/verify-email-account-activation/).
*/
@@ -110,14 +118,6 @@ export type EnablesLinkMethod = boolean
export type OverrideTheBaseURLWhichShouldBeUsedAsTheBaseForRecoveryAndVerificationLinks =
string
export type HowLongALinkIsValidFor = string
-export type EnablesLoginAndRegistrationWithTheCodeMethod = boolean
-export type EnablesLoginFlowsCodeMethodToFulfilMFARequests = boolean
-/**
- * This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.
- */
-export type PasswordlessLoginFallbackEnabled = boolean
-export type EnablesCodeMethod = boolean
-export type HowLongACodeIsValidFor = string
export type EnablesUsernameEmailAndPasswordMethod = boolean
/**
* Allows changing the default HIBP host to a self hosted version.
@@ -178,6 +178,19 @@ export type RelyingPartyRPConfig =
origins: string[]
[k: string]: unknown | undefined
}
+export type EnablesThePasskeyMethod = boolean
+/**
+ * A name to help the user identify this RP.
+ */
+export type RelyingPartyDisplayName = string
+/**
+ * The id must be a subset of the domain currently in the browser.
+ */
+export type RelyingPartyIdentifier = string
+/**
+ * A list of explicit RP origins. If left empty, this defaults to either `origin` or `id`, prepended with the current protocol schema (HTTP or HTTPS).
+ */
+export type RelyingPartyOrigins = string[]
export type EnablesOpenIDConnectMethod = boolean
/**
* Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.
@@ -202,6 +215,7 @@ export type SelfServiceOIDCProvider = SelfServiceOIDCProvider1 & {
requested_claims?: OpenIDConnectClaims
organization_id?: OrganizationID
additional_id_token_audiences?: AdditionalClientIdsAllowedWhenUsingIDTokenSubmission
+ claims_source?: ClaimsSource
}
export type SelfServiceOIDCProvider1 = {
[k: string]: unknown | undefined
@@ -230,7 +244,9 @@ export type Provider =
| "dingtalk"
| "patreon"
| "linkedin"
+ | "linkedin_v2"
| "lark"
+ | "x"
export type OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons =
string
/**
@@ -262,6 +278,10 @@ export type ApplePrivateKey = string
*/
export type OrganizationID = string
export type AdditionalClientIdsAllowedWhenUsingIDTokenSubmission = string[]
+/**
+ * Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`
+ */
+export type ClaimsSource = "id_token" | "userinfo"
/**
* A list and configuration of OAuth2 and OpenID Connect providers Ory Kratos should integrate with.
*/
@@ -517,14 +537,26 @@ export type AddExemptURLsToPrivateIPRanges = string[]
* If enabled allows Ory Sessions to be cached. Only effective in the Ory Network.
*/
export type EnableOrySessionsCaching = boolean
+/**
+ * Set how long Ory Sessions are cached on the edge. If unset, the session expiry will be used. Only effective in the Ory Network.
+ */
+export type SetOrySessionEdgeCachingMaximumAge = string
/**
* If enabled allows new flow transitions using `continue_with` items.
*/
export type EnableNewFlowTransitionsUsingContinueWithItems = boolean
/**
- * Secifies which organizations are available. Only effective in the Ory Network.
+ * If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future.
+ */
+export type EnableFasterSessionExtension = boolean
+/**
+ * Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network.
*/
export type Organizations = unknown[]
+/**
+ * A fallback URL template used when looking up identity schemas.
+ */
+export type FallbackURLTemplateForIdentitySchemas = string
export interface OryKratosConfiguration2 {
selfservice: {
@@ -551,10 +583,12 @@ export interface OryKratosConfiguration2 {
lifespan?: string
before?: SelfServiceBeforeRegistration
after?: SelfServiceAfterRegistration
+ enable_legacy_one_step?: DisableTwoStepRegistration
}
login?: {
ui_url?: LoginUIURL
lifespan?: string
+ style?: LoginFlowStyle
before?: SelfServiceBeforeLogin
after?: SelfServiceAfterLogin
}
@@ -565,6 +599,7 @@ export interface OryKratosConfiguration2 {
}
}
methods?: {
+ b2b?: SingleSignOnForB2B
profile?: {
enabled?: EnablesProfileManagementMethod
}
@@ -572,13 +607,22 @@ export interface OryKratosConfiguration2 {
enabled?: EnablesLinkMethod
config?: LinkConfiguration
}
- code?: {
- passwordless_enabled?: EnablesLoginAndRegistrationWithTheCodeMethod
- mfa_enabled?: EnablesLoginFlowsCodeMethodToFulfilMFARequests
- passwordless_login_fallback_enabled?: PasswordlessLoginFallbackEnabled
- enabled?: EnablesCodeMethod
- config?: CodeConfiguration
- }
+ code?:
+ | {
+ passwordless_enabled?: true
+ mfa_enabled?: false
+ [k: string]: unknown | undefined
+ }
+ | {
+ mfa_enabled?: true
+ passwordless_enabled?: false
+ [k: string]: unknown | undefined
+ }
+ | {
+ mfa_enabled?: false
+ passwordless_enabled?: false
+ [k: string]: unknown | undefined
+ }
password?: {
enabled?: EnablesUsernameEmailAndPasswordMethod
config?: PasswordConfiguration
@@ -594,6 +638,10 @@ export interface OryKratosConfiguration2 {
enabled?: EnablesTheWebAuthnMethod
config?: WebAuthnConfiguration
}
+ passkey?: {
+ enabled?: EnablesThePasskeyMethod
+ config?: PasskeyConfiguration
+ }
oidc?: SpecifyOpenIDConnectAndOAuth2Configuration
}
}
@@ -701,6 +749,16 @@ export interface OryKratosConfiguration2 {
}
earliest_possible_extend?: EarliestPossibleSessionExtension
}
+ security?: {
+ account_enumeration?: {
+ /**
+ * Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet.
+ */
+ mitigate?: boolean
+ [k: string]: unknown | undefined
+ }
+ [k: string]: unknown | undefined
+ }
version?: TheKratosVersionThisConfigIsWrittenFor
dev?: boolean
help?: boolean
@@ -720,6 +778,7 @@ export interface OryKratosConfiguration2 {
clients?: GlobalOutgoingNetworkSettings
feature_flags?: FeatureFlags
organizations?: Organizations
+ enterprise?: EnterpriseFeatures
}
export interface SelfServiceAfterSettings {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
@@ -727,6 +786,7 @@ export interface SelfServiceAfterSettings {
totp?: SelfServiceAfterSettingsAuthMethod
oidc?: SelfServiceAfterSettingsAuthMethod
webauthn?: SelfServiceAfterSettingsAuthMethod
+ passkey?: SelfServiceAfterSettingsAuthMethod
lookup_secret?: SelfServiceAfterSettingsAuthMethod
profile?: SelfServiceAfterSettingsMethod
hooks?: SelfServiceHooks
@@ -762,6 +822,7 @@ export interface SelfServiceAfterRegistration {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
password?: SelfServiceAfterRegistrationMethod
webauthn?: SelfServiceAfterRegistrationMethod
+ passkey?: SelfServiceAfterRegistrationMethod
oidc?: SelfServiceAfterRegistrationMethod
code?: SelfServiceAfterRegistrationMethod
hooks?: SelfServiceHooks
@@ -788,6 +849,7 @@ export interface SelfServiceAfterLogin {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
password?: SelfServiceAfterDefaultLoginMethod
webauthn?: SelfServiceAfterDefaultLoginMethod
+ passkey?: SelfServiceAfterDefaultLoginMethod
oidc?: SelfServiceAfterOIDCLoginMethod
code?: SelfServiceAfterDefaultLoginMethod
totp?: SelfServiceAfterDefaultLoginMethod
@@ -796,6 +858,8 @@ export interface SelfServiceAfterLogin {
| SelfServiceWebHook
| SelfServiceSessionRevokerHook
| SelfServiceRequireVerifiedAddressHook
+ | SelfServiceVerificationHook
+ | SelfServiceShowVerificationUIHook
| B2BSSOHook
)[]
}
@@ -805,11 +869,16 @@ export interface SelfServiceAfterDefaultLoginMethod {
| SelfServiceSessionRevokerHook
| SelfServiceRequireVerifiedAddressHook
| SelfServiceWebHook
+ | SelfServiceVerificationHook
+ | SelfServiceShowVerificationUIHook
)[]
}
export interface SelfServiceRequireVerifiedAddressHook {
hook: "require_verified_address"
}
+export interface SelfServiceVerificationHook {
+ hook: "verification"
+}
export interface SelfServiceAfterOIDCLoginMethod {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
hooks?: (
@@ -851,6 +920,25 @@ export interface SelfServiceAfterRecovery {
export interface SelfServiceBeforeRecovery {
hooks?: SelfServiceHooks
}
+/**
+ * Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code.
+ */
+export interface SingleSignOnForB2B {
+ config?: {
+ organizations?: {
+ /**
+ * The ID of the organization.
+ */
+ id?: string
+ /**
+ * The label of the organization.
+ */
+ label?: string
+ domains?: string[]
+ [k: string]: unknown | undefined
+ }[]
+ }
+}
/**
* Additional configuration for the link strategy.
*/
@@ -859,13 +947,6 @@ export interface LinkConfiguration {
lifespan?: HowLongALinkIsValidFor
[k: string]: unknown | undefined
}
-/**
- * Additional configuration for the code strategy.
- */
-export interface CodeConfiguration {
- lifespan?: HowLongACodeIsValidFor
- [k: string]: unknown | undefined
-}
/**
* Define how passwords are validated.
*/
@@ -884,6 +965,15 @@ export interface WebAuthnConfiguration {
passwordless?: UseForPasswordlessFlows
rp?: RelyingPartyRPConfig
}
+export interface PasskeyConfiguration {
+ rp?: RelyingPartyRPConfig1
+}
+export interface RelyingPartyRPConfig1 {
+ display_name: RelyingPartyDisplayName
+ id: RelyingPartyIdentifier
+ origins?: RelyingPartyOrigins
+ [k: string]: unknown | undefined
+}
export interface SpecifyOpenIDConnectAndOAuth2Configuration {
enabled?: EnablesOpenIDConnectMethod
config?: {
@@ -963,6 +1053,7 @@ export interface CourierConfiguration {
login_code?: {
valid?: {
email: EmailCourierTemplate
+ sms?: SmsCourierTemplate
}
}
}
@@ -1080,7 +1171,7 @@ export interface WebHookAuthBasicAuthProperties {
* Configures outgoing emails using the SMTP protocol.
*/
export interface SMTPConfiguration {
- connection_uri: SMTPConnectionString
+ connection_uri?: SMTPConnectionString
client_cert_path?: SMTPClientCertificatePath
client_key_path?: SMTPClientPrivateKeyPath
from_address?: SMTPSenderAddress
@@ -1375,5 +1466,13 @@ export interface GlobalHTTPClientConfiguration {
}
export interface FeatureFlags {
cacheable_sessions?: EnableOrySessionsCaching
+ cacheable_sessions_max_age?: SetOrySessionEdgeCachingMaximumAge
use_continue_with_transitions?: EnableNewFlowTransitionsUsingContinueWithItems
+ faster_session_extend?: EnableFasterSessionExtension
+}
+/**
+ * Specifies enterprise features. Only effective in the Ory Network or with a valid license.
+ */
+export interface EnterpriseFeatures {
+ identity_schema_fallback_url_template?: FallbackURLTemplateForIdentitySchemas
}
diff --git a/text/id.go b/text/id.go
index a466caec0f8c..edff417a2738 100644
--- a/text/id.go
+++ b/text/id.go
@@ -31,6 +31,7 @@ const (
InfoSelfServiceLoginCodeMFA // 1010019
InfoSelfServiceLoginCodeMFAHint // 1010020
InfoSelfServiceLoginPasskey // 1010021
+ InfoSelfServiceLoginPassword // 1010022
)
const (
@@ -86,21 +87,21 @@ const (
)
const (
- InfoNodeLabel ID = 1070000 + iota // 1070000
- InfoNodeLabelInputPassword // 1070001
- InfoNodeLabelGenerated // 1070002
- InfoNodeLabelSave // 1070003
- InfoNodeLabelID // 1070004
- InfoNodeLabelSubmit // 1070005
- InfoNodeLabelVerifyOTP // 1070006
- InfoNodeLabelEmail // 1070007
- InfoNodeLabelResendOTP // 1070008
- InfoNodeLabelContinue // 1070009
- InfoNodeLabelRecoveryCode // 1070010
- InfoNodeLabelVerificationCode // 1070011
- InfoNodeLabelRegistrationCode // 1070012
- InfoNodeLabelLoginCode // 1070013
- InfoNodeLabelLoginAndLinkCredential
+ InfoNodeLabel ID = 1070000 + iota // 1070000
+ InfoNodeLabelInputPassword // 1070001
+ InfoNodeLabelGenerated // 1070002
+ InfoNodeLabelSave // 1070003
+ InfoNodeLabelID // 1070004
+ InfoNodeLabelSubmit // 1070005
+ InfoNodeLabelVerifyOTP // 1070006
+ InfoNodeLabelEmail // 1070007
+ InfoNodeLabelResendOTP // 1070008
+ InfoNodeLabelContinue // 1070009
+ InfoNodeLabelRecoveryCode // 1070010
+ InfoNodeLabelVerificationCode // 1070011
+ InfoNodeLabelRegistrationCode // 1070012
+ InfoNodeLabelLoginCode // 1070013
+ InfoNodeLabelLoginAndLinkCredential // 1070014
)
const (
@@ -148,6 +149,7 @@ const (
ErrorValidationPasswordTooManyBreaches
ErrorValidationNoCodeUser
ErrorValidationTraitsMismatch
+ ErrorValidationAccountNotFound
)
const (
diff --git a/text/message_login.go b/text/message_login.go
index ec627458a028..9312a21e97a2 100644
--- a/text/message_login.go
+++ b/text/message_login.go
@@ -89,6 +89,14 @@ func NewInfoLoginTOTP() *Message {
}
}
+func NewInfoLoginPassword() *Message {
+ return &Message{
+ ID: InfoSelfServiceLoginPassword,
+ Text: "Sign in with password",
+ Type: Info,
+ }
+}
+
func NewInfoLoginLookup() *Message {
return &Message{
ID: InfoLoginLookup,
@@ -182,7 +190,7 @@ func NewErrorValidationVerificationNoStrategyFound() *Message {
func NewInfoSelfServiceLoginWebAuthn() *Message {
return &Message{
ID: InfoSelfServiceLoginWebAuthn,
- Text: "Use security key",
+ Text: "Sign in with hardware key",
Type: Info,
}
}
@@ -198,7 +206,7 @@ func NewInfoSelfServiceLoginPasskey() *Message {
func NewInfoSelfServiceContinueLoginWebAuthn() *Message {
return &Message{
ID: InfoSelfServiceLoginContinueWebAuthn,
- Text: "Continue with security key",
+ Text: "Sign in with hardware key",
Type: Info,
}
}
@@ -239,7 +247,7 @@ func NewInfoSelfServiceLoginCode() *Message {
return &Message{
ID: InfoSelfServiceLoginCode,
Type: Info,
- Text: "Sign in with code",
+ Text: "Send sign in code",
}
}
diff --git a/text/message_validation.go b/text/message_validation.go
index c10fddead805..2fd1e4c2d28d 100644
--- a/text/message_validation.go
+++ b/text/message_validation.go
@@ -257,6 +257,14 @@ func NewErrorValidationInvalidCredentials() *Message {
}
}
+func NewErrorValidationAccountNotFound() *Message {
+ return &Message{
+ ID: ErrorValidationAccountNotFound,
+ Text: "This account does not exist or has no login method configured.",
+ Type: Error,
+ }
+}
+
func NewErrorValidationDuplicateCredentials() *Message {
return &Message{
ID: ErrorValidationDuplicateCredentials,
diff --git a/ui/node/attributes.go b/ui/node/attributes.go
index 9611b5828dff..6ad55d46a0e2 100644
--- a/ui/node/attributes.go
+++ b/ui/node/attributes.go
@@ -3,7 +3,12 @@
package node
-import "github.com/ory/kratos/text"
+import (
+ "fmt"
+
+ "github.com/ory/kratos/text"
+ "github.com/ory/kratos/x/webauthnx/js"
+)
const (
InputAttributeTypeText UiNodeInputAttributeType = "text"
@@ -53,6 +58,9 @@ type Attributes interface {
// swagger:ignore
GetNodeType() UiNodeType
+
+ // swagger:ignore
+ Matches(other Attributes) bool
}
// InputAttributes represents the attributes of an input node
@@ -91,12 +99,29 @@ type InputAttributes struct {
// OnClick may contain javascript which should be executed on click. This is primarily
// used for WebAuthn.
+ //
+ // Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.
OnClick string `json:"onclick,omitempty"`
+ // OnClickTrigger may contain a WebAuthn trigger which should be executed on click.
+ //
+ // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.
+ OnClickTrigger js.WebAuthnTriggers `json:"onclickTrigger,omitempty"`
+
// OnLoad may contain javascript which should be executed on load. This is primarily
// used for WebAuthn.
+ //
+ // Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.
OnLoad string `json:"onload,omitempty"`
+ // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.
+ //
+ // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.
+ OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"`
+
+ // MaxLength may contain the input's maximum length.
+ MaxLength int `json:"maxlength,omitempty"`
+
// NodeType represents this node's types. It is a mirror of `node.type` and
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input".
//
@@ -267,6 +292,99 @@ func (a *ScriptAttributes) ID() string {
return a.Identifier
}
+func (a *InputAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*InputAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if len(ot.Type) > 0 && a.Type != ot.Type {
+ return false
+ }
+
+ if ot.FieldValue != nil && fmt.Sprintf("%v", a.FieldValue) != fmt.Sprintf("%v", ot.FieldValue) {
+ return false
+ }
+
+ if len(ot.Name) > 0 && a.Name != ot.Name {
+ return false
+ }
+
+ return true
+}
+
+func (a *ImageAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*ImageAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if len(ot.Source) > 0 && a.Source != ot.Source {
+ return false
+ }
+
+ return true
+}
+
+func (a *AnchorAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*AnchorAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if len(ot.HREF) > 0 && a.HREF != ot.HREF {
+ return false
+ }
+
+ return true
+}
+
+func (a *TextAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*TextAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ return true
+}
+
+func (a *ScriptAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*ScriptAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if ot.Type != "" && a.Type != ot.Type {
+ return false
+ }
+
+ if ot.Source != "" && a.Source != ot.Source {
+ return false
+ }
+
+ return true
+}
+
func (a *InputAttributes) SetValue(value interface{}) {
a.FieldValue = value
}
diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go
index b63ac9365a51..176c1a25b42e 100644
--- a/ui/node/attributes_input.go
+++ b/ui/node/attributes_input.go
@@ -38,6 +38,12 @@ func WithRequiredInputAttribute(a *InputAttributes) {
a.Required = true
}
+func WithMaxLengthInputAttribute(maxLength int) func(a *InputAttributes) {
+ return func(a *InputAttributes) {
+ a.MaxLength = maxLength
+ }
+}
+
func WithInputAttributes(f func(a *InputAttributes)) func(a *InputAttributes) {
return func(a *InputAttributes) {
f(a)
diff --git a/ui/node/attributes_test.go b/ui/node/attributes_test.go
index 218919e1145e..62c2316d3c9f 100644
--- a/ui/node/attributes_test.go
+++ b/ui/node/attributes_test.go
@@ -21,6 +21,70 @@ func TestIDs(t *testing.T) {
assert.EqualValues(t, "foo", (&ScriptAttributes{Identifier: "foo"}).ID())
}
+func TestMatchesAnchorAttributes(t *testing.T) {
+ assert.True(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{Identifier: "foo"}))
+ assert.True(t, (&AnchorAttributes{HREF: "bar"}).Matches(&AnchorAttributes{HREF: "bar"}))
+ assert.False(t, (&AnchorAttributes{HREF: "foo"}).Matches(&AnchorAttributes{HREF: "bar"}))
+ assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{HREF: "bar"}))
+
+ assert.True(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "bar"}))
+ assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "baz"}))
+ assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "bar", HREF: "bar"}))
+
+ assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+}
+
+func TestMatchesImageAttributes(t *testing.T) {
+ assert.True(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"}))
+ assert.True(t, (&ImageAttributes{Source: "bar"}).Matches(&ImageAttributes{Source: "bar"}))
+ assert.False(t, (&ImageAttributes{Source: "foo"}).Matches(&ImageAttributes{Source: "bar"}))
+ assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Source: "bar"}))
+
+ assert.True(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "bar"}))
+ assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "baz"}))
+ assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "bar", Source: "bar"}))
+
+ assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+}
+
+func TestMatchesInputAttributes(t *testing.T) {
+ // Test when other is not of type *InputAttributes
+ var attr Attributes = &ImageAttributes{}
+ inputAttr := &InputAttributes{Name: "foo"}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when ID is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
+ inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when Type is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
+ inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeNumber}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when FieldValue is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
+ inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "baz"}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when Name is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
+ inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when all fields are the same
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
+ inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
+ assert.True(t, inputAttr.Matches(attr))
+}
+
+func TestMatchesTextAttributes(t *testing.T) {
+ assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+ assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+ assert.False(t, (&TextAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"}))
+}
+
func TestNodeEncode(t *testing.T) {
script := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ScriptAttributes{}})
assert.EqualValues(t, Script, gjson.Get(script, "attributes.node_type").String())
diff --git a/ui/node/node.go b/ui/node/node.go
index e08295b827f4..66e1b72fa8b6 100644
--- a/ui/node/node.go
+++ b/ui/node/node.go
@@ -39,16 +39,17 @@ func (t UiNodeType) String() string {
type UiNodeGroup string
const (
- DefaultGroup UiNodeGroup = "default"
- PasswordGroup UiNodeGroup = "password"
- OpenIDConnectGroup UiNodeGroup = "oidc"
- ProfileGroup UiNodeGroup = "profile"
- LinkGroup UiNodeGroup = "link"
- CodeGroup UiNodeGroup = "code"
- TOTPGroup UiNodeGroup = "totp"
- LookupGroup UiNodeGroup = "lookup_secret"
- WebAuthnGroup UiNodeGroup = "webauthn"
- PasskeyGroup UiNodeGroup = "passkey"
+ DefaultGroup UiNodeGroup = "default"
+ PasswordGroup UiNodeGroup = "password"
+ OpenIDConnectGroup UiNodeGroup = "oidc"
+ ProfileGroup UiNodeGroup = "profile"
+ LinkGroup UiNodeGroup = "link"
+ CodeGroup UiNodeGroup = "code"
+ TOTPGroup UiNodeGroup = "totp"
+ LookupGroup UiNodeGroup = "lookup_secret"
+ WebAuthnGroup UiNodeGroup = "webauthn"
+ PasskeyGroup UiNodeGroup = "passkey"
+ IdentifierFirstGroup UiNodeGroup = "identifier_first"
)
func (g UiNodeGroup) String() string {
@@ -218,6 +219,7 @@ func SortUseOrder(keysInOrder []string) func(*sortOptions) {
options.keysInOrder = keysInOrder
}
}
+
func SortUseOrderAppend(keysInOrder []string) func(*sortOptions) {
return func(options *sortOptions) {
options.keysInOrderAppend = keysInOrder
@@ -353,6 +355,37 @@ func (n *Nodes) Append(node *Node) {
*n = append(*n, node)
}
+func (n *Nodes) RemoveMatching(node *Node) {
+ if n == nil {
+ return
+ }
+
+ var r Nodes
+ for k, v := range *n {
+ if !(*n)[k].Matches(node) {
+ r = append(r, v)
+ }
+ }
+
+ *n = r
+}
+
+func (n *Node) Matches(needle *Node) bool {
+ if len(needle.ID()) > 0 && n.ID() != needle.ID() {
+ return false
+ }
+
+ if needle.Type != "" && n.Type != needle.Type {
+ return false
+ }
+
+ if needle.Group != "" && n.Group != needle.Group {
+ return false
+ }
+
+ return n.Attributes.Matches(needle.Attributes)
+}
+
func (n *Node) UnmarshalJSON(data []byte) error {
var attr Attributes
switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) {
diff --git a/ui/node/node_test.go b/ui/node/node_test.go
index f8867b98c2a3..e8a85bc9cd9f 100644
--- a/ui/node/node_test.go
+++ b/ui/node/node_test.go
@@ -11,6 +11,8 @@ import (
"path/filepath"
"testing"
+ "github.com/ory/kratos/text"
+
"github.com/ory/x/assertx"
"github.com/ory/kratos/corpx"
@@ -193,3 +195,64 @@ func TestNodeJSON(t *testing.T) {
require.EqualError(t, json.NewDecoder(bytes.NewReader(json.RawMessage(`{"type": "foo"}`))).Decode(&n), "unexpected node type: foo")
})
}
+
+func TestMatchesNode(t *testing.T) {
+ // Test when ID is different
+ node1 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
+ assert.False(t, node1.Matches(node2))
+
+ // Test when Type is different
+ node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 = &node.Node{Type: node.Text, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ assert.False(t, node1.Matches(node2))
+
+ // Test when Group is different
+ node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 = &node.Node{Type: node.Input, Group: node.OpenIDConnectGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ assert.False(t, node1.Matches(node2))
+
+ // Test when all fields are the same
+ node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ assert.True(t, node1.Matches(node2))
+}
+
+func TestRemoveMatchingNodes(t *testing.T) {
+ nodes := node.Nodes{
+ &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}},
+ &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}},
+ &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}},
+ }
+
+ // Test when node to remove is present
+ nodeToRemove := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
+ nodes.RemoveMatching(nodeToRemove)
+ assert.Len(t, nodes, 2)
+ for _, n := range nodes {
+ assert.NotEqual(t, nodeToRemove.ID(), n.ID())
+ }
+
+ // Test when node to remove is not present
+ nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "qux"}}
+ nodes.RemoveMatching(nodeToRemove)
+ assert.Len(t, nodes, 2) // length should remain the same
+
+ // Test when node to remove is present
+ nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}}
+ ui := &container.Container{
+ Nodes: nodes,
+ }
+
+ ui.GetNodes().RemoveMatching(nodeToRemove)
+ assert.Len(t, *ui.GetNodes(), 1)
+ for _, n := range *ui.GetNodes() {
+ assert.NotEqual(t, "bar", n.ID())
+ assert.NotEqual(t, "baz", n.ID())
+ }
+
+ ui.Nodes.Append(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
+ assert.NotNil(t, ui.Nodes.Find("method"))
+ ui.GetNodes().RemoveMatching(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit))
+ assert.Nil(t, ui.Nodes.Find("method"))
+}
diff --git a/x/webauthnx/js/trigger.go b/x/webauthnx/js/trigger.go
new file mode 100644
index 000000000000..7b236191ce8e
--- /dev/null
+++ b/x/webauthnx/js/trigger.go
@@ -0,0 +1,22 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package js
+
+import "fmt"
+
+// swagger:enum WebAuthnTriggers
+type WebAuthnTriggers string
+
+const (
+ WebAuthnTriggersWebAuthnRegistration WebAuthnTriggers = "oryWebAuthnRegistration"
+ WebAuthnTriggersWebAuthnLogin WebAuthnTriggers = "oryWebAuthnLogin"
+ WebAuthnTriggersPasskeyLogin WebAuthnTriggers = "oryPasskeyLogin"
+ WebAuthnTriggersPasskeyLoginAutocompleteInit WebAuthnTriggers = "oryPasskeyLoginAutocompleteInit"
+ WebAuthnTriggersPasskeyRegistration WebAuthnTriggers = "oryPasskeyRegistration"
+ WebAuthnTriggersPasskeySettingsRegistration WebAuthnTriggers = "oryPasskeySettingsRegistration"
+)
+
+func (r WebAuthnTriggers) String() string {
+ return fmt.Sprintf("window.%s", string(r))
+}
diff --git a/x/webauthnx/js/trigger_test.go b/x/webauthnx/js/trigger_test.go
new file mode 100644
index 000000000000..97f9dc00ee77
--- /dev/null
+++ b/x/webauthnx/js/trigger_test.go
@@ -0,0 +1,14 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package js
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToString(t *testing.T) {
+ assert.Equal(t, "window.oryWebAuthnRegistration", WebAuthnTriggersWebAuthnRegistration.String())
+}
diff --git a/x/webauthnx/js/webauthn.js b/x/webauthnx/js/webauthn.js
index 61a7cb8f976d..638bd4ece082 100644
--- a/x/webauthnx/js/webauthn.js
+++ b/x/webauthnx/js/webauthn.js
@@ -32,7 +32,7 @@
}
function __oryWebAuthnLogin(
- opt,
+ options,
resultQuerySelector = '*[name="webauthn_login"]',
triggerQuerySelector = '*[name="webauthn_login_trigger"]',
) {
@@ -40,6 +40,12 @@
alert("This browser does not support WebAuthn!")
}
+ const triggerEl = document.querySelector(triggerQuerySelector)
+ let opt = options
+ if (!opt) {
+ opt = JSON.parse(triggerEl.value)
+ }
+
opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge)
opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map(
function (value) {
@@ -71,7 +77,7 @@
},
})
- document.querySelector(triggerQuerySelector).closest("form").submit()
+ triggerEl.closest("form").submit()
})
.catch((err) => {
alert(err)
@@ -79,7 +85,7 @@
}
function __oryWebAuthnRegistration(
- opt,
+ options,
resultQuerySelector = '*[name="webauthn_register"]',
triggerQuerySelector = '*[name="webauthn_register_trigger"]',
) {
@@ -87,6 +93,12 @@
alert("This browser does not support WebAuthn!")
}
+ const triggerEl = document.querySelector(triggerQuerySelector)
+ let opt = options
+ if (!opt) {
+ opt = JSON.parse(triggerEl.value)
+ }
+
opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id)
opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge)
@@ -118,14 +130,14 @@
},
})
- document.querySelector(triggerQuerySelector).closest("form").submit()
+ triggerEl.closest("form").submit()
})
.catch((err) => {
alert(err)
})
}
- window.__oryPasskeyLoginAutocompleteInit = async function () {
+ async function __oryPasskeyLoginAutocompleteInit () {
const dataEl = document.getElementsByName("passkey_challenge")[0]
const resultEl = document.getElementsByName("passkey_login")[0]
const identifierEl = document.getElementsByName("identifier")[0]
@@ -195,7 +207,7 @@
})
}
- window.__oryPasskeyLogin = function () {
+ function __oryPasskeyLogin () {
const dataEl = document.getElementsByName("passkey_challenge")[0]
const resultEl = document.getElementsByName("passkey_login")[0]
@@ -262,7 +274,7 @@
})
}
- window.__oryPasskeyRegistration = function () {
+ function __oryPasskeyRegistration () {
const dataEl = document.getElementsByName("passkey_create_data")[0]
const resultEl = document.getElementsByName("passkey_register")[0]
@@ -373,8 +385,21 @@
})
}
+ // Deprecated naming with underscores - kept for support with Ory Elements v0
window.__oryWebAuthnLogin = __oryWebAuthnLogin
window.__oryWebAuthnRegistration = __oryWebAuthnRegistration
window.__oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration
+ window.__oryPasskeyLogin = __oryPasskeyLogin
+ window.__oryPasskeyRegistration = __oryPasskeyRegistration
+ window.__oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit
+
+ // Current naming - use with Ory Elements v1
+ window.oryWebAuthnLogin = __oryWebAuthnLogin
+ window.oryWebAuthnRegistration = __oryWebAuthnRegistration
+ window.oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration
+ window.oryPasskeyLogin = __oryPasskeyLogin
+ window.oryPasskeyRegistration = __oryPasskeyRegistration
+ window.oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit
+
window.__oryWebAuthnInitialized = true
})()
diff --git a/x/webauthnx/nodes.go b/x/webauthnx/nodes.go
index 76fac1c397cd..309f49f80e9d 100644
--- a/x/webauthnx/nodes.go
+++ b/x/webauthnx/nodes.go
@@ -10,6 +10,8 @@ import (
"fmt"
"net/url"
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/ory/x/stringsx"
"github.com/ory/x/urlx"
@@ -21,7 +23,10 @@ import (
func NewWebAuthnConnectionTrigger(options string) *node.Node {
return node.NewInputField(node.WebAuthnRegisterTrigger, "", node.WebAuthnGroup,
node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) {
- a.OnClick = "window.__oryWebAuthnRegistration(" + options + ")"
+ //nolint:staticcheck
+ a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnRegistration, options)
+ a.OnClickTrigger = js.WebAuthnTriggersWebAuthnRegistration
+ a.FieldValue = options
}))
}
@@ -44,7 +49,10 @@ func NewWebAuthnConnectionInput() *node.Node {
func NewWebAuthnLoginTrigger(options string) *node.Node {
return node.NewInputField(node.WebAuthnLoginTrigger, "", node.WebAuthnGroup,
node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) {
- a.OnClick = "window.__oryWebAuthnLogin(" + options + ")"
+ //nolint:staticcheck
+ a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnLogin, options)
+ a.FieldValue = options
+ a.OnClickTrigger = js.WebAuthnTriggersWebAuthnLogin
}))
}