diff --git a/driver/registry_default.go b/driver/registry_default.go index bc7a31e76c2b..417d5a0ed18d 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -755,6 +755,10 @@ func (m *RegistryDefault) RegistrationCodePersister() code.RegistrationCodePersi return m.Persister() } +func (m *RegistryDefault) LoginCodePersister() code.LoginCodePersister { + return m.Persister() +} + func (m *RegistryDefault) Persister() persistence.Persister { return m.persister } diff --git a/persistence/reference.go b/persistence/reference.go index 0e034d72c74d..56a7ca1712df 100644 --- a/persistence/reference.go +++ b/persistence/reference.go @@ -52,6 +52,7 @@ type Persister interface { code.RecoveryCodePersister code.VerificationCodePersister code.RegistrationCodePersister + code.LoginCodePersister CleanupDatabase(context.Context, time.Duration, time.Duration, int) error Close(context.Context) error diff --git a/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json new file mode 100644 index 000000000000..5e429ce3d9ca --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_code/f1f66a69-ce02-4a12-9591-9e02dda30a0d.json @@ -0,0 +1,5 @@ +{ + "id": "f1f66a69-ce02-4a12-9591-9e02dda30a0d", + "expires_at": "2022-08-18T08:28:18Z", + "issued_at": "2022-08-18T07:28:18Z" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json new file mode 100644 index 000000000000..e5fcc4a278f1 --- /dev/null +++ b/persistence/sql/migratest/fixtures/registration_flow/69c80296-36cd-4afc-921a-15369cac5bf0.json @@ -0,0 +1,14 @@ +{ + "id": "69c80296-36cd-4afc-921a-15369cac5bf0", + "type": "browser", + "expires_at": "2013-10-07T08:23:19Z", + "issued_at": "2013-10-07T08:23:19Z", + "request_url": "http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=", + "active": "password", + "ui": { + "action": "", + "method": "", + "nodes": null + }, + "state": "" +} diff --git a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json index 8947653d90e7..ce1272433edf 100644 --- a/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json +++ b/persistence/sql/migratest/fixtures/registration_flow/ef18b06e-4700-4021-9949-ef783cd86be1.json @@ -9,5 +9,6 @@ "action": "", "method": "", "nodes": null - } + }, + "state": "" } diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 36126f147935..798afd54dc79 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -73,7 +73,8 @@ func CompareWithFixture(t *testing.T, actual interface{}, prefix string, id stri func TestMigrations_SQLite(t *testing.T) { t.Parallel() sqlite, err := pop.NewConnection(&pop.ConnectionDetails{ - URL: "sqlite3://" + filepath.Join(os.TempDir(), x.NewUUID().String()) + ".sql?_fk=true"}) + URL: "sqlite3://" + filepath.Join(os.TempDir(), x.NewUUID().String()) + ".sql?_fk=true", + }) require.NoError(t, err) require.NoError(t, sqlite.Open()) @@ -105,7 +106,6 @@ func TestMigrations_Cockroach(t *testing.T) { } func testDatabase(t *testing.T, db string, c *pop.Connection) { - ctx := context.Background() l := logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel)) @@ -372,6 +372,40 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_code"), found) }) + t.Run("case=registration_code", func(t *testing.T) { + wg.Add(1) + defer wg.Done() + t.Parallel() + + var ids []code.RegistrationCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "registration_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "registration_code"), found) + }) + + t.Run("case=login_code", func(t *testing.T) { + wg.Add(1) + defer wg.Done() + t.Parallel() + + var ids []code.LoginCode + require.NoError(t, c.All(&ids)) + require.NotEmpty(t, ids) + + var found []string + for _, id := range ids { + found = append(found, id.ID.String()) + CompareWithFixture(t, id, "login_code", id.ID.String()) + } + migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "login_code"), found) + }) + t.Run("suite=constraints", func(t *testing.T) { // This is not really a parallel test, but we have to mark it parallel so the other tests run first. t.Parallel() diff --git a/persistence/sql/migratest/testdata/20230707133700_testdata.sql b/persistence/sql/migratest/testdata/20230707133700_testdata.sql new file mode 100644 index 000000000000..20d877f5be1e --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133700_testdata.sql @@ -0,0 +1,23 @@ +INSERT INTO selfservice_login_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, created_at, + updated_at, forced, type, ui, internal_context, oauth2_login_challenge_data) +VALUES ('00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', + '0c175792-3aad-4795-ad03-972e8a88f94c', + 'http://kratos:4433/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login/self-service/browser/flows/login', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', '', + 'fpeVSZ9ZH7YvUkhXsOVEIssxbfauh5lcoQSYxTcN0XkMneg1L42h+HtvisjlNjBF4ElcD2jApCHoJYq2u9sVWg==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', false, 'api', '{}', '{"foo":"bar"}', 'challenge data'); + + +INSERT INTO identity_login_codes (id, code, used_at, expires_at, issued_at, selfservice_login_flow_id, + identity_verifiable_address_id, created_at, updated_at, nid) +VALUES ('bd292366-af32-4ba6-bdf0-11d6d1a217f3', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'00b1517f-2467-4aaf-b0a5-82b4a27dcaf5', +'d4718a67-aec2-418d-8173-6ebc7bde3b86', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'0c175792-3aad-4795-ad03-972e8a88f94c' +) diff --git a/persistence/sql/migratest/testdata/20230707133701_testdata.sql b/persistence/sql/migratest/testdata/20230707133701_testdata.sql new file mode 100644 index 000000000000..54dd77c78ff6 --- /dev/null +++ b/persistence/sql/migratest/testdata/20230707133701_testdata.sql @@ -0,0 +1,22 @@ +INSERT INTO selfservice_registration_flows (id, nid, request_url, issued_at, expires_at, active_method, csrf_token, + created_at, updated_at, type, ui, internal_context, oauth2_login_challenge) +VALUES ('69c80296-36cd-4afc-921a-15369cac5bf0', '884f556e-eb3a-4b9f-bee3-11345642c6c0', + 'http://kratos:4433/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge/self-service/browser/flows/registration?login_challenge=', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', + 'password', 'vYYuhWXBfXKzBC+BlnbDmXfBKsUWY6SU/v04gHF9GYzPjFP51RXDPOc57R7Dpbf+XLkbPNAkmem33Crz/avdrw==', + '2013-10-07 08:23:19', '2013-10-07 08:23:19', 'browser', '{}', '{"foo":"bar"}', + '3caddfd5-9903-4bce-83ff-cae36f42dff7'); + +INSERT INTO identity_registration_codes (id, address, code, used_at, expires_at, issued_at, selfservice_registration_flow_id, + created_at, updated_at, nid) +VALUES ('f1f66a69-ce02-4a12-9591-9e02dda30a0d', +'example@example.com', +'7eb71370d8497734ec78dfe613bf0f08967e206d2b5c2fc1243be823cfcd57a7', +null, +'2022-08-18 08:28:18', +'2022-08-18 07:28:18', +'69c80296-36cd-4afc-921a-15369cac5bf0', +'2022-08-18 07:28:18', +'2022-08-18 07:28:18', +'884f556e-eb3a-4b9f-bee3-11345642c6c0' +) diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql new file mode 100644 index 000000000000..79a48193bfe8 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.down.sql @@ -0,0 +1,4 @@ +DROP TABLE identity_login_codes; + +ALTER TABLE selfservice_login_flows DROP submit_count; +ALTER TABLE selfservice_login_flows DROP skip_csrf_check; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.down.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.down.sql new file mode 100644 index 000000000000..79a48193bfe8 --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.down.sql @@ -0,0 +1,4 @@ +DROP TABLE identity_login_codes; + +ALTER TABLE selfservice_login_flows DROP submit_count; +ALTER TABLE selfservice_login_flows DROP skip_csrf_check; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql new file mode 100644 index 000000000000..147934207b9f --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.mysql.up.sql @@ -0,0 +1,33 @@ +CREATE TABLE identity_login_codes +( + id CHAR(36) NOT NULL PRIMARY KEY, + code VARCHAR (64) NOT NULL, -- HMACed value of the actual code + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id CHAR(36), + identity_verifiable_address_id CHAR(36), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid CHAR(36) NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT identity_login_codes_identity_verifiable_addresses_id_fk + FOREIGN KEY (identity_verifiable_address_id) + REFERENCES identity_verifiable_addresses (id) + ON DELETE cascade +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_identity_verifiable_address_id_idx ON identity_login_codes (identity_verifiable_address_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_login_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql new file mode 100644 index 000000000000..94350da1425d --- /dev/null +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_login_code.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE identity_login_codes +( + id UUID NOT NULL PRIMARY KEY, + code VARCHAR (64) NOT NULL, -- HMACed value of the actual code + used_at timestamp NULL DEFAULT NULL, + expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', + selfservice_login_flow_id UUID NOT NULL, + identity_verifiable_address_id UUID NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + nid UUID NOT NULL, + CONSTRAINT identity_login_codes_selfservice_login_flows_id_fk + FOREIGN KEY (selfservice_login_flow_id) + REFERENCES selfservice_login_flows (id) + ON DELETE cascade, + CONSTRAINT identity_login_codes_networks_id_fk + FOREIGN KEY (nid) + REFERENCES networks (id) + ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT identity_login_codes_identity_verifiable_addresses_id_fk + FOREIGN KEY (identity_verifiable_address_id) + REFERENCES identity_verifiable_addresses (id) + ON DELETE cascade +); + +CREATE INDEX identity_login_codes_nid_flow_id_idx ON identity_login_codes (nid, selfservice_login_flow_id); +CREATE INDEX identity_login_codes_identity_verifiable_address_id_idx ON identity_login_codes (identity_verifiable_address_id); +CREATE INDEX identity_login_codes_id_nid_idx ON identity_login_codes (id, nid); + +ALTER TABLE selfservice_login_flows ADD submit_count int NOT NULL DEFAULT 0; +ALTER TABLE selfservice_login_flows ADD skip_csrf_check boolean NOT NULL DEFAULT FALSE; diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.down.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql similarity index 100% rename from persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.down.sql rename to persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.down.sql diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.down.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.down.sql similarity index 100% rename from persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.down.sql rename to persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.down.sql diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql similarity index 100% rename from persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.up.sql rename to persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.mysql.up.sql diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.up.sql b/persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql similarity index 100% rename from persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.up.sql rename to persistence/sql/migrations/sql/20230707133700000001_identity_registration_code.up.sql diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go index 1f29a1860e35..f95853e7d2bc 100644 --- a/persistence/sql/persister_login.go +++ b/persistence/sql/persister_login.go @@ -5,17 +5,22 @@ package sql import ( "context" + "crypto/subtle" "fmt" "time" "github.com/gobuffalo/pop/v6" + "github.com/pkg/errors" "github.com/gofrs/uuid" "github.com/ory/x/sqlcon" + "github.com/ory/kratos/identity" "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/code" ) var _ login.FlowPersister = new(Persister) @@ -84,3 +89,122 @@ func (p *Persister) DeleteExpiredLoginFlows(ctx context.Context, expiresAt time. } return nil } + +func (p *Persister) CreateLoginCode(ctx context.Context, codeParams *code.CreateLoginCodeParams) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginCode") + defer span.End() + + now := time.Now() + + loginCode := &code.LoginCode{ + VerifiableAddressID: uuid.NullUUID{UUID: codeParams.VerifiableAddress.ID, Valid: true}, + CodeHMAC: p.hmacValue(ctx, codeParams.RawCode), + IssuedAt: now, + ExpiresAt: now.UTC().Add(p.r.Config().SelfServiceCodeMethodLifespan(ctx)), + FlowID: codeParams.FlowID, + NID: p.NetworkID(ctx), + ID: uuid.Nil, + } + + if err := p.GetConnection(ctx).Create(loginCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return loginCode, nil +} + +func (p *Persister) UseLoginCode(ctx context.Context, flowID uuid.UUID, codeVal string) (*code.LoginCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UseLoginCode") + defer span.End() + + var loginCode *code.LoginCode + + nid := p.NetworkID(ctx) + flowTableName := new(login.Flow).TableName(ctx) + + if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { + //#nosec G201 -- TableName is static + if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET submit_count = submit_count + 1 WHERE id = ? AND nid = ?", flowTableName), flowID, nid).Exec()); err != nil { + return err + } + + var submitCount int + // Because MySQL does not support "RETURNING" clauses, but we need the updated `submit_count` later on. + //#nosec G201 -- TableName is static + if err := sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("SELECT submit_count FROM %s WHERE id = ? AND nid = ?", flowTableName), flowID, nid).First(&submitCount)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + // Return no error, as that would roll back the transaction + return nil + } + return err + } + + if submitCount > 5 { + return errors.WithStack(code.ErrCodeSubmittedTooOften) + } + + var loginCodes []code.LoginCode + if err = sqlcon.HandleError(tx.Where("nid = ? AND selfservice_login_flow_id = ?", nid, flowID).All(&loginCodes)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + return err + } + return nil + } + + secrets: + + for _, secret := range p.r.Config().SecretsSession(ctx) { + suppliedCode := []byte(p.hmacValueWithSecret(ctx, codeVal, secret)) + for i := range loginCodes { + code := loginCodes[i] + if subtle.ConstantTimeCompare([]byte(code.CodeHMAC), suppliedCode) == 0 { + // Not the supplied code + continue + } + loginCode = &code + break secrets + } + } + + if loginCode == nil || !loginCode.IsValid() { + // Return no error, as that would roll back the transaction + return nil + } + + var verifiableAddress identity.VerifiableAddress + if err := tx.Where("nid = ? AND id = ?", nid, loginCode.VerifiableAddressID).First(&verifiableAddress); err != nil { + if err = sqlcon.HandleError(err); !errors.Is(err, sqlcon.ErrNoRows) { + return err + } + return err + } + + loginCode.VerifiableAddress = &verifiableAddress + + //#nosec G201 -- TableName is static + return sqlcon.HandleError(tx.RawQuery(fmt.Sprintf("UPDATE %s SET used_at = ? WHERE id = ? AND nid = ?", loginCode.TableName(ctx)), time.Now().UTC(), loginCode.ID, nid).Exec()) + })); err != nil { + return nil, err + } + + if loginCode == nil { + return nil, code.ErrCodeNotFound + } + + if loginCode.IsExpired() { + return nil, flow.NewFlowExpiredError(loginCode.ExpiresAt) + } + + if loginCode.WasUsed() { + return nil, code.ErrCodeAlreadyUsed + } + + return nil, nil +} + +func (p *Persister) DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteLoginCodesOfFlow") + defer span.End() + + //#nosec G201 -- TableName is static + return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE selfservice_login_flow_id = ? AND nid = ?", new(code.LoginCode).TableName(ctx)), flowID, p.NetworkID(ctx)).Exec() +} diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go index 74160b8d2566..9f1a144ffc2d 100644 --- a/selfservice/flow/login/sort.go +++ b/selfservice/flow/login/sort.go @@ -15,6 +15,7 @@ func sortNodes(ctx context.Context, n node.Nodes) error { node.OpenIDConnectGroup, node.DefaultGroup, node.WebAuthnGroup, + node.CodeGroup, node.PasswordGroup, node.TOTPGroup, node.LookupGroup, 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 71c0f353e6c6..6681ed6af2cf 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 @@ -43,7 +43,7 @@ "attributes": { "name": "csrf_token", "type": "hidden", - "value": "ysjIrDUcUoRLGdMGXi6pnZlUgkb9Ug9G6s8v12xzVhkUUbXWtSVZlrbxPAr8V0YkdhVK8kVP4UHsuZD8azDx3w==", + "value": "cpIqhfoy4/wdxgPc6IQBXeN1MF9zMtv+wT+Rg0k0FR0QKZ1eqdqRub7owJfTj3N0O4gl5feI9lsnylK2Il1zlA==", "required": true, "disabled": false, "node_type": "input" @@ -51,32 +51,12 @@ "messages": [], "meta": {} }, - { - "type": "input", - "group": "code", - "attributes": { - "name": "code", - "type": "text", - "required": true, - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": { - "label": { - "id": 1070011, - "text": "Verification code", - "type": "info" - } - } - }, { "type": "input", "group": "code", "attributes": { "name": "method", "type": "hidden", - "value": "code", "disabled": false, "node_type": "input" }, diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go index 89364f044d37..8888b760cdc6 100644 --- a/selfservice/strategy/code/code_login.go +++ b/selfservice/strategy/code/code_login.go @@ -4,22 +4,85 @@ package code import ( + "context" "database/sql" + "time" "github.com/gofrs/uuid" + "github.com/ory/kratos/identity" ) -type LoginRegistrationCode struct { - // ID is the primary key +type LoginCode struct { + // ID represents the tokens's unique ID. // // required: true // type: string // format: uuid ID uuid.UUID `json:"id" db:"id" faker:"-"` - // CodeHMAC represents the HMACed value of the login/registration code - CodeHMAC string `json:"-" db:"code_hmac"` + // VerifiableAddress links this code to a verification address. + // required: true + VerifiableAddress *identity.VerifiableAddress `json:"verification_address" belongs_to:"identity_verifiable_addresses"` + + // CodeHMAC represents the HMACed value of the verification code + CodeHMAC string `json:"-" db:"code"` // UsedAt is the timestamp of when the code was used or null if it wasn't yet UsedAt sql.NullTime `json:"-" db:"used_at"` + + // ExpiresAt is the time (UTC) when the token expires. + // required: true + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` + + // IssuedAt is the time (UTC) when the token was issued. + // required: true + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + // VerifiableAddressID is a helper struct field for gobuffalo.pop. + VerifiableAddressID uuid.NullUUID `json:"-" faker:"-" db:"identity_verifiable_address_id"` + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID `json:"-" faker:"-" db:"selfservice_registration_flow_id"` + + NID uuid.UUID `json:"-" faker:"-" db:"nid"` + IdentityID uuid.UUID `json:"identity_id" faker:"-" db:"identity_id"` +} + +func (LoginCode) TableName(ctx context.Context) string { + return "identity_login_codes" +} + +func (f LoginCode) IsExpired() bool { + return f.ExpiresAt.Before(time.Now()) +} + +func (r LoginCode) WasUsed() bool { + return r.UsedAt.Valid +} + +func (f LoginCode) IsValid() bool { + return !f.IsExpired() && !f.WasUsed() +} + +type CreateLoginCodeParams struct { + // Code represents the recovery code + RawCode string + + // ExpiresAt is the time (UTC) when the code expires. + // required: true + ExpiresIn time.Duration + + // VerifiableAddress links this code to a verification address. + VerifiableAddress *identity.VerifiableAddress + + // FlowID is a helper struct field for gobuffalo.pop. + FlowID uuid.UUID + + IdentityID uuid.UUID } diff --git a/selfservice/strategy/code/registration_code.go b/selfservice/strategy/code/code_registration.go similarity index 100% rename from selfservice/strategy/code/registration_code.go rename to selfservice/strategy/code/code_registration.go diff --git a/selfservice/strategy/code/persistence.go b/selfservice/strategy/code/persistence.go index 84e36f617f4d..9c8b8a107947 100644 --- a/selfservice/strategy/code/persistence.go +++ b/selfservice/strategy/code/persistence.go @@ -40,4 +40,14 @@ type ( DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*RegistrationCode, error) } + + LoginCodePersistenceProvider interface { + LoginCodePersister() LoginCodePersister + } + + LoginCodePersister interface { + CreateLoginCode(context.Context, *CreateLoginCodeParams) (*LoginCode, error) + UseLoginCode(ctx context.Context, flowID uuid.UUID, code string) (*LoginCode, error) + DeleteLoginCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + } ) diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index 27bc2e1ebb85..f16b035e6d8e 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -128,6 +128,12 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { f.SetState(flow.StateChooseMethod) } + switch f.GetFlowName() { + case flow.VerificationFlow, flow.RecoveryFlow: + f.GetUI().ResetMessages() + break + } + switch f.GetState() { case flow.StateChooseMethod: @@ -147,19 +153,24 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { case flow.StateEmailSent: var codeMetaLabel *text.Message + var message *text.Message switch f.GetFlowName() { case flow.RecoveryFlow: codeMetaLabel = text.NewInfoNodeLabelRecoveryCode() + message = text.NewRecoveryEmailWithCodeSent() break case flow.VerificationFlow: codeMetaLabel = text.NewInfoNodeLabelVerificationCode() + message = text.NewVerificationEmailWithCodeSent() break case flow.LoginFlow: codeMetaLabel = text.NewInfoNodeLabelLoginCode() + message = text.NewLoginEmailWithCodeSent() break case flow.RegistrationFlow: codeMetaLabel = text.NewInfoNodeLabelRegistrationCode() + message = text.NewRegistrationEmailWithCodeSent() break } @@ -171,6 +182,8 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), ) + f.GetUI().Messages.Set(message) + if f.GetFlowName() == flow.VerificationFlow { f.GetUI().Messages.Set(text.NewVerificationEmailWithCodeSent()) } diff --git a/selfservice/strategy/lookup/.snapshots/TestCompleteLogin-case=lookup_payload_is_set_when_identity_has_lookup.json b/selfservice/strategy/lookup/.snapshots/TestCompleteLogin-case=lookup_payload_is_set_when_identity_has_lookup.json index a553621e76f0..b732a1221b9f 100644 --- a/selfservice/strategy/lookup/.snapshots/TestCompleteLogin-case=lookup_payload_is_set_when_identity_has_lookup.json +++ b/selfservice/strategy/lookup/.snapshots/TestCompleteLogin-case=lookup_payload_is_set_when_identity_has_lookup.json @@ -1,30 +1,11 @@ [ - { - "attributes": { - "disabled": false, - "name": "method", - "node_type": "input", - "type": "submit" - }, - "group": "code", - "messages": [], - "meta": { - "label": { - "id": 1070005, - "text": "Submit", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, "name": "csrf_token", "node_type": "input", "required": true, - "type": "hidden", - "value": "bHNqd3NiMTQwdDdxbmtwbmxlZWh0cGtrY29na3VwMDc=" + "type": "hidden" }, "group": "default", "messages": [], 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 27a422ea7e3b..ff40c45ad144 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 @@ -1,30 +1,11 @@ [ - { - "attributes": { - "disabled": false, - "name": "method", - "node_type": "input", - "type": "submit" - }, - "group": "code", - "messages": [], - "meta": { - "label": { - "id": 1070005, - "text": "Submit", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, "name": "csrf_token", "node_type": "input", "required": true, - "type": "hidden", - "value": "dWJ6a3ppaDJ3MHM3NHA5enNyeXJzeDlsZnNzeGJqaG8=" + "type": "hidden" }, "group": "default", "messages": [], 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 0a7494871340..eb348b172ab6 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 @@ -82,5 +82,6 @@ ] }, "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } 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 0a7494871340..eb348b172ab6 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 @@ -82,5 +82,6 @@ ] }, "refresh": false, - "requested_aal": "aal1" + "requested_aal": "aal1", + "state": "" } diff --git a/text/id.go b/text/id.go index 7a91f8f58353..6f988ee98757 100644 --- a/text/id.go +++ b/text/id.go @@ -23,6 +23,7 @@ const ( InfoSelfServiceLoginContinueWebAuthn // 1010011 InfoSelfServiceLoginWebAuthnPasswordless // 1010012 InfoSelfServiceLoginContinue // 1010013 + InfoSelfServiceLoginEmailWithCodeSent // 1010014 ) const ( diff --git a/text/message_login.go b/text/message_login.go index 0a751530ddf0..b1e4d0534cf4 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -183,3 +183,12 @@ func NewInfoSelfServiceLoginContinue() *Message { Type: Info, } } + +func NewLoginEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceLoginEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the login.", + Context: context(nil), + } +} diff --git a/text/message_recovery.go b/text/message_recovery.go index b6ed3a6be683..788b88f2808b 100644 --- a/text/message_recovery.go +++ b/text/message_recovery.go @@ -49,15 +49,6 @@ func NewRecoveryEmailWithCodeSent() *Message { } } -func NewRegistrationEmailWithCodeSent() *Message { - return &Message{ - ID: InfoSelfServiceRegistrationEmailWithCodeSent, - Type: Info, - Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the registration.", - Context: context(nil), - } -} - func NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed() *Message { return &Message{ ID: ErrorValidationRecoveryTokenInvalidOrAlreadyUsed, diff --git a/text/message_registration.go b/text/message_registration.go index be9135cd06bb..64198a61a859 100644 --- a/text/message_registration.go +++ b/text/message_registration.go @@ -54,3 +54,12 @@ func NewInfoSelfServiceRegistrationRegisterWebAuthn() *Message { Type: Info, } } + +func NewRegistrationEmailWithCodeSent() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationEmailWithCodeSent, + Type: Info, + Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the registration.", + Context: context(nil), + } +} diff --git a/x/xsql/sql.go b/x/xsql/sql.go index f2354d082ca1..7ee2591bcd74 100644 --- a/x/xsql/sql.go +++ b/x/xsql/sql.go @@ -28,6 +28,8 @@ import ( func CleanSQL(t testing.TB, c *pop.Connection) { ctx := context.Background() for _, table := range []string{ + new(code.LoginCode).TableName(ctx), + new(code.RegistrationCode).TableName(ctx), new(continuity.Container).TableName(ctx), new(courier.MessageDispatch).TableName(), new(courier.Message).TableName(ctx),