diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index a69732b19a9e..1c04dcaf2ef4 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -155,6 +155,8 @@ type Flow struct { // ReturnToVerification contains the redirect URL for the verification flow. ReturnToVerification string `json:"-" db:"-"` + + isAccountLinkingFlow bool `json:"-" db:"-"` } var _ flow.Flow = new(Flow) diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index e252a23e7011..fe76240b5221 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -118,6 +118,12 @@ func WithFormErrorMessage(messages []text.Message) FlowOption { } } +func WithIsAccountLinking() FlowOption { + return func(f *Flow) { + f.isAccountLinkingFlow = true + } +} + func (h *Handler) NewLoginFlow(w http.ResponseWriter, r *http.Request, ft flow.Type, opts ...FlowOption) (*Flow, *session.Session, error) { conf := h.d.Config() f, err := NewFlow(conf, conf.SelfServiceFlowLoginRequestLifespan(r.Context()), h.d.GenerateCSRFToken(r), r, ft) @@ -226,7 +232,7 @@ preLoginHook: // 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()): + case h.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(r.Context()) && !f.isAccountLinkingFlow: populateErr = strategy.PopulateLoginMethodIdentifierFirstIdentification(r, f) default: populateErr = strategy.PopulateLoginMethodFirstFactor(r, f) diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..c87e844c54e3 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,185 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm 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": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "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": "email-exist-with-password-strategy@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "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" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "duplicateIdentifier": "email-exist-with-password-strategy@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 84e84a6ccf8e..14c2c26f8e50 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -680,7 +680,7 @@ func (s *Strategy) populateAccountLinkingUI(ctx context.Context, lf *login.Flow, nodes := []*node.Node{} for _, n := range lf.UI.Nodes { // We don't want to touch nodes unecessary nodes - if n.Meta == nil || n.Meta.Label == nil || n.Group == "default" { + if n.Meta == nil || n.Meta.Label == nil || n.Group == node.DefaultGroup { nodes = append(nodes, n) continue } diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index d4cacb7c8ed9..946d60d3ade8 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -261,7 +261,7 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r opts = append(opts, login.WithFormErrorMessage(rf.UI.Messages)) } - opts = append(opts, login.WithInternalContext(rf.InternalContext)) + opts = append(opts, login.WithInternalContext(rf.InternalContext), login.WithIsAccountLinking()) lf, _, err := s.d.LoginHandler().NewLoginFlow(w, r, rf.Type, opts...) if err != nil { diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index eda0fa53c18b..c3c8abd2ccce 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -1095,6 +1095,14 @@ func TestStrategy(t *testing.T) { snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", "ui.nodes.4.attributes.value", "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl")) }) + t.Run("case=should fail registration id_first strategy enabled", func(t *testing.T) { + conf.Set(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, "valid", action, url.Values{}) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", "ui.nodes.4.attributes.value", "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl")) + }) + t.Run("case=should fail login", func(t *testing.T) { r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid")