diff --git a/embedx/config.schema.json b/embedx/config.schema.json index ece7f175b20b..4476c39d2d18 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -947,6 +947,9 @@ "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "code": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "hooks": { "$ref": "#/definitions/selfServiceHooks" } @@ -2740,4 +2743,4 @@ "selfservice" ], "additionalProperties": false -} \ No newline at end of file +} diff --git a/identity/credentials_code.go b/identity/credentials_code.go index c6d2a0512229..2f6a32861ce0 100644 --- a/identity/credentials_code.go +++ b/identity/credentials_code.go @@ -3,7 +3,9 @@ package identity -import "github.com/ory/x/sqlxx" +import ( + "database/sql" +) // CredentialsOTP represents an OTP code // @@ -13,5 +15,5 @@ type CredentialsCode struct { CodeHMAC string `json:"code_hmac"` // UsedAt indicates whether and when a recovery code was used. - UsedAt sqlxx.NullTime `json:"used_at,omitempty"` + UsedAt sql.NullTime `json:"used_at,omitempty"` } diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.cockroach.down.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.cockroach.down.sql new file mode 100644 index 000000000000..84f10f939a12 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'code'; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.cockroach.up.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.cockroach.up.sql new file mode 100644 index 000000000000..47e0cf0b2b34 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '14f3b7e2-8725-4068-be39-8a796485fd97', 'code' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'code'); diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.mysql.down.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.mysql.down.sql new file mode 100644 index 000000000000..84f10f939a12 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'code'; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.mysql.up.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.mysql.up.sql new file mode 100644 index 000000000000..47e0cf0b2b34 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '14f3b7e2-8725-4068-be39-8a796485fd97', 'code' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'code'); diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.postgres.down.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.postgres.down.sql new file mode 100644 index 000000000000..84f10f939a12 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'code'; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.postgres.up.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.postgres.up.sql new file mode 100644 index 000000000000..47e0cf0b2b34 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '14f3b7e2-8725-4068-be39-8a796485fd97', 'code' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'code'); diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.sqlite.down.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.sqlite.down.sql new file mode 100644 index 000000000000..84f10f939a12 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.sqlite.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'code'; diff --git a/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.sqlite.up.sql b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.sqlite.up.sql new file mode 100644 index 000000000000..47e0cf0b2b34 --- /dev/null +++ b/persistence/sql/migrations/sql/20230712173852000000_credential_types_code.sqlite.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '14f3b7e2-8725-4068-be39-8a796485fd97', 'code' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'code'); diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index 558a65c9cd3e..29a82a87a0e2 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -5,6 +5,7 @@ package code import ( "context" + "database/sql" "encoding/json" "net/http" @@ -81,7 +82,15 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, rf *registration. return s.PopulateMethod(r, rf) } -func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flow, p *UpdateRegistrationFlowWithCodeMethod, i *identity.Identity) error { +type options func(*identity.Identity) error + +func WithCredentials(code string, usedAt sql.NullTime) options { + return func(i *identity.Identity) error { + return i.SetCredentialsWithConfig(identity.CredentialsTypeCodeAuth, identity.Credentials{Type: identity.CredentialsTypePassword, Identifiers: []string{}}, &identity.CredentialsCode{CodeHMAC: code, UsedAt: usedAt}) + } +} + +func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flow, p *UpdateRegistrationFlowWithCodeMethod, i *identity.Identity, opts ...options) error { f.TransientPayload = p.TransientPayload if len(p.Traits) == 0 { p.Traits = json.RawMessage("{}") @@ -89,10 +98,16 @@ func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flo // we explicitly set the Code credentials type i.Traits = identity.Traits(p.Traits) - if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, &identity.CredentialsCode{CodeHMAC: ""}); err != nil { + if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, &identity.CredentialsCode{CodeHMAC: "", UsedAt: sql.NullTime{}}); err != nil { return err } + for _, opt := range opts { + if err := opt(i); err != nil { + return err + } + } + // Validate the identity if err := s.deps.IdentityValidator().Validate(ctx, i); err != nil { return err @@ -210,19 +225,19 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat // we are in the second submission state of the flow // we need to check the code and update the identity - // first lets ensure the traits are still correct - if err := s.handleIdentityTraits(r.Context(), f, &p, i); err != nil { + // Step 1: Attempt to use the code + registrationCode, err := s.deps.RegistrationCodePersister().UseRegistrationCode(r.Context(), f.ID, p.Code) + if err != nil { return s.HandleRegistrationError(w, r, f, &p, err) } - _, err := s.deps.RegistrationCodePersister().UseRegistrationCode(r.Context(), f.ID, p.Code) - if err != nil { + // Step 2: The code was correct, populate the Identity credentials and traits + if err := s.handleIdentityTraits(r.Context(), f, &p, i, WithCredentials(registrationCode.CodeHMAC, registrationCode.UsedAt)); err != nil { return s.HandleRegistrationError(w, r, f, &p, err) } // since nothing has errored yet, we can assume that the code is correct // and we can update the registration flow - f.SetState(flow.StatePassedChallenge) if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(r.Context(), f); err != nil { diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index 0485f1dcec6e..2a0702b27cb0 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -30,6 +30,9 @@ func TestRegistrationCodeStrategy(t *testing.T) { testhelpers.StrategyEnable(t, conf, string(identity.CredentialsTypeCodeAuth), true) conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ + {"hook": "session"}, + }) _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg)