From b2830eedffcf3b14083e6777be417c964999c205 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Wed, 23 Feb 2022 16:43:39 +0100 Subject: [PATCH 01/15] refactor: move credential configs for oidc and password --- cmd/identities/get_test.go | 6 +- identity/credentials_oidc.go | 57 ++++++++++++++++++ identity/credentials_password.go | 9 +++ identity/handler.go | 60 ++++++++++++++++++- identity/handler_test.go | 6 +- identity/identity.go | 17 ++++++ selfservice/strategy/oidc/strategy.go | 7 +-- selfservice/strategy/oidc/strategy_login.go | 4 +- .../strategy/oidc/strategy_registration.go | 4 +- .../strategy/oidc/strategy_settings.go | 20 +++---- .../strategy/oidc/strategy_settings_test.go | 2 +- selfservice/strategy/oidc/strategy_test.go | 16 ++--- selfservice/strategy/oidc/types.go | 43 ------------- selfservice/strategy/password/login.go | 4 +- selfservice/strategy/password/login_test.go | 4 +- selfservice/strategy/password/registration.go | 9 +-- selfservice/strategy/password/settings.go | 13 ++-- selfservice/strategy/password/strategy.go | 2 +- selfservice/strategy/password/types.go | 6 -- 19 files changed, 181 insertions(+), 108 deletions(-) create mode 100644 identity/credentials_oidc.go create mode 100644 identity/credentials_password.go diff --git a/cmd/identities/get_test.go b/cmd/identities/get_test.go index 911da181e28f..fcbd3309902c 100644 --- a/cmd/identities/get_test.go +++ b/cmd/identities/get_test.go @@ -7,8 +7,6 @@ import ( "testing" "github.com/ory/kratos/cmd/identities" - "github.com/ory/kratos/selfservice/strategy/oidc" - "github.com/ory/x/assertx" "github.com/ory/kratos/x" @@ -55,7 +53,7 @@ func TestGetCmd(t *testing.T) { t.Run("case=gets a single identity with oidc credentials", func(t *testing.T) { applyCredentials := func(identifier, accessToken, refreshToken, idToken string, encrypt bool) identity.Credentials { - toJson := func(c oidc.CredentialsConfig) []byte { + toJson := func(c identity.CredentialsOIDC) []byte { out, err := json.Marshal(&c) require.NoError(t, err) return out @@ -69,7 +67,7 @@ func TestGetCmd(t *testing.T) { return identity.Credentials{ Type: identity.CredentialsTypeOIDC, Identifiers: []string{"bar:" + identifier}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ { Subject: "foo", Provider: "bar", diff --git a/identity/credentials_oidc.go b/identity/credentials_oidc.go new file mode 100644 index 000000000000..406643fac636 --- /dev/null +++ b/identity/credentials_oidc.go @@ -0,0 +1,57 @@ +package identity + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + + "github.com/ory/kratos/x" +) + +// CredentialsOIDC is contains the configuration for credentials of the type oidc. +// +// swagger:model identityCredentialsOidc +type CredentialsOIDC struct { + Providers []CredentialsOIDCProvider `json:"providers"` +} + +// CredentialsOIDCProvider is contains a specific OpenID COnnect credential for a particular connection (e.g. Google). +// +// swagger:model identityCredentialsOidcProvider +type CredentialsOIDCProvider struct { + Subject string `json:"subject"` + Provider string `json:"provider"` + InitialIDToken string `json:"initial_id_token"` + InitialAccessToken string `json:"initial_access_token"` + InitialRefreshToken string `json:"initial_refresh_token"` +} + +// NewCredentialsOIDC creates a new OIDC credential. +func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject string) (*Credentials, error) { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(CredentialsOIDC{ + Providers: []CredentialsOIDCProvider{ + { + Subject: subject, + Provider: provider, + InitialIDToken: idToken, + InitialAccessToken: accessToken, + InitialRefreshToken: refreshToken, + }}, + }); err != nil { + return nil, errors.WithStack(x.PseudoPanic. + WithDebugf("Unable to encode password options to JSON: %s", err)) + } + + return &Credentials{ + Type: CredentialsTypeOIDC, + Identifiers: []string{OIDCUniqueID(provider, subject)}, + Config: b.Bytes(), + }, nil +} + +func OIDCUniqueID(provider, subject string) string { + return fmt.Sprintf("%s:%s", provider, subject) +} diff --git a/identity/credentials_password.go b/identity/credentials_password.go new file mode 100644 index 000000000000..9a9783e56ddb --- /dev/null +++ b/identity/credentials_password.go @@ -0,0 +1,9 @@ +package identity + +// CredentialsPassword is contains the configuration for credentials of the type password. +// +// swagger:model identityCredentialsPassword +type CredentialsPassword struct { + // HashedPassword is a hash-representation of the password. + HashedPassword string `json:"hashed_password"` +} diff --git a/identity/handler.go b/identity/handler.go index 92a9adcf6039..bd9d4f3dd1e2 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -202,12 +202,58 @@ type AdminCreateIdentityBody struct { // required: true Traits json.RawMessage `json:"traits"` + // Credentials represents all credentials that can be used for authenticating this identity. + // + // Use this structure to import credentials for a user. + Credentials *AdminIdentityImportCredentials `json:"credentials"` + + // VerifiableAddresses contains all the addresses that can be verified by the user. + // + // Use this structure to import verified addresses for an identity. Please keep in mind + // that the address needs to be represented in the Identity Schema or this field will be overwritten + // on the next identity update. + VerifiableAddresses []VerifiableAddress `json:"verifiable_addresses"` + + // RecoveryAddresses contains all the addresses that can be used to recover an identity. + // + // Use this structure to import recovery addresses for an identity. Please keep in mind + // that the address needs to be represented in the Identity Schema or this field will be overwritten + // on the next identity update. + RecoveryAddresses []RecoveryAddress `json:"recovery_addresses"` + // State is the identity's state. // // required: false State State `json:"state"` } +// swagger:model adminIdentityImportCredentials +type AdminIdentityImportCredentials struct { + // Password if set will import a password credential. + Password *AdminIdentityImportCredentialsPassword `json:"password,omitempty"` + + // OIDC if set will import an OIDC credential. + OIDC *AdminIdentityImportCredentialsOIDC `json:"oidc,omitempty"` +} + +// swagger:model AdminCreateIdentityImportCredentialsPassword +type AdminIdentityImportCredentialsPassword struct { + // The hashed password in [PHC format]( https://www.ory.sh/docs/kratos/concepts/credentials/username-email-password#hashed-password-format) + HashedPassword string `json:"hashed_password"` + + // The password in plain text if no hash is available. + Password string `json:"password"` +} + +// swagger:model AdminCreateIdentityImportCredentialsOIDC +type AdminIdentityImportCredentialsOIDC struct { + // The subject (`sub`) of the OpenID Connect connection. Usually the `sub` field of the ID Token. + Subject string `json:"subject"` + + // The OpenID Connect provider to link the subject to. Usually something like `google` or `github`. + Provider string `json:"provider"` +} + // swagger:route POST /identities v0alpha2 adminCreateIdentity // // Create an Identity @@ -249,7 +295,19 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa } state = cr.State } - i := &Identity{SchemaID: cr.SchemaID, Traits: []byte(cr.Traits), State: state, StateChangedAt: &stateChangedAt} + + i := &Identity{ + SchemaID: cr.SchemaID, + Traits: []byte(cr.Traits), + State: state, + StateChangedAt: &stateChangedAt, + //Credentials: cr.Credentials, + VerifiableAddresses: cr.VerifiableAddresses, + RecoveryAddresses: cr.RecoveryAddresses, + } + //i.Traits = identity.Traits(p.Traits) + //i.SetCredentials(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}, Config: co}) + if err := h.r.IdentityManager().Create(r.Context(), i); err != nil { h.r.Writer().WriteError(w, r, err) return diff --git a/identity/handler_test.go b/identity/handler_test.go index d3d7aae1ebc5..03d5cc7ef0c8 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -11,8 +11,6 @@ import ( "testing" "time" - "github.com/ory/kratos/selfservice/strategy/oidc" - "github.com/bxcodec/faker/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -174,7 +172,7 @@ func TestHandler(t *testing.T) { } iId := x.NewUUID() - toJson := func(c oidc.CredentialsConfig) []byte { + toJson := func(c identity.CredentialsOIDC) []byte { out, err := json.Marshal(&c) require.NoError(t, err) return out @@ -186,7 +184,7 @@ func TestHandler(t *testing.T) { identity.CredentialsTypeOIDC: { Type: identity.CredentialsTypeOIDC, Identifiers: []string{"bar:" + identifier}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ { Subject: "foo", Provider: "bar", diff --git a/identity/identity.go b/identity/identity.go index 2b026303106c..472416cb941d 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -189,6 +189,23 @@ func (i *Identity) SetCredentials(t CredentialsType, c Credentials) { i.Credentials[t] = c } +func (i *Identity) SetCredentialsWithConfig(t CredentialsType, c Credentials, conf interface{}) (err error) { + i.lock().Lock() + defer i.lock().Unlock() + if i.Credentials == nil { + i.Credentials = make(map[CredentialsType]Credentials) + } + + c.Config, err = json.Marshal(conf) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode %s credentials to JSON: %s", t, err)) + } + + c.Type = t + i.Credentials[t] = c + return nil +} + func (i *Identity) DeleteCredentialsType(t CredentialsType) { i.lock().Lock() defer i.lock().Unlock() diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index c2a09b2f12c3..858820b14e02 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "path/filepath" "strings" @@ -116,7 +115,7 @@ type authCodeContainer struct { func (s *Strategy) CountActiveCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { for _, c := range cc { if c.Type == s.ID() && gjson.ValidBytes(c.Config) { - var conf CredentialsConfig + var conf identity.CredentialsOIDC if err = json.Unmarshal(c.Config, &conf); err != nil { return 0, errors.WithStack(err) } @@ -366,10 +365,6 @@ func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps htt } } -func uid(provider, subject string) string { - return fmt.Sprintf("%s:%s", provider, subject) -} - func (s *Strategy) populateMethod(r *http.Request, c *container.Container, message func(provider string) *text.Message) error { conf, err := s.Config(r.Context()) if err != nil { diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 1199915b3761..a1bbdacbcb6b 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -72,7 +72,7 @@ type SubmitSelfServiceLoginFlowWithOidcMethodBody struct { } func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *authCodeContainer) (*registration.Flow, error) { - i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, uid(provider.Config().ID, claims.Subject)) + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) if err != nil { if errors.Is(err, sqlcon.ErrNoRows) { // If no account was found we're "manually" creating a new registration flow and redirecting the browser @@ -104,7 +104,7 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login return nil, s.handleError(w, r, a, provider.Config().ID, nil, err) } - var o CredentialsConfig + var o identity.CredentialsOIDC if err := json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o); err != nil { return nil, s.handleError(w, r, a, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()))) } diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 3dcc82e76280..dd4effa9f79e 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -154,7 +154,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a *registration.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *authCodeContainer) (*login.Flow, error) { - if _, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, uid(provider.Config().ID, claims.Subject)); err == nil { + if _, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)); err == nil { // If the identity already exists, we should perform the login flow instead. // That will execute the "pre registration" hook which allows to e.g. disallow this flow. The registration @@ -254,7 +254,7 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } - creds, err := NewCredentials(it, cat, crt, provider.Config().ID, claims.Subject) + creds, err := identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject) if err != nil { return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 70383c442a51..e9232568befc 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -78,7 +78,7 @@ func (s *Strategy) linkedProviders(ctx context.Context, r *http.Request, conf *C return nil, nil } - var available CredentialsConfig + var available identity.CredentialsOIDC if err := json.Unmarshal(creds.Config, &available); err != nil { return nil, errors.WithStack(err) } @@ -115,7 +115,7 @@ func (s *Strategy) linkedProviders(ctx context.Context, r *http.Request, conf *C } func (s *Strategy) linkableProviders(ctx context.Context, r *http.Request, conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) { - var available CredentialsConfig + var available identity.CredentialsOIDC creds, ok := confidential.GetCredentials(s.ID()) if ok { if err := json.Unmarshal(creds.Config, &available); err != nil { @@ -394,18 +394,18 @@ func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdat return s.handleSettingsError(w, r, ctxUpdate, p, err) } - var conf CredentialsConfig + var conf identity.CredentialsOIDC creds, err := i.ParseCredentials(s.ID(), &conf) if errors.Is(err, herodot.ErrNotFound) { var err error - if creds, err = NewCredentials(it, cat, crt, provider.Config().ID, claims.Subject); err != nil { + if creds, err = identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject); err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } } else if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } else { - creds.Identifiers = append(creds.Identifiers, uid(provider.Config().ID, claims.Subject)) - conf.Providers = append(conf.Providers, ProviderCredentialsConfig{ + creds.Identifiers = append(creds.Identifiers, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) + conf.Providers = append(conf.Providers, identity.CredentialsOIDCProvider{ Subject: claims.Subject, Provider: provider.Config().ID, InitialAccessToken: cat, InitialRefreshToken: crt, @@ -448,20 +448,20 @@ func (s *Strategy) unlinkProvider(w http.ResponseWriter, r *http.Request, ctxUpd return s.handleSettingsError(w, r, ctxUpdate, p, err) } - var cc CredentialsConfig + var cc identity.CredentialsOIDC creds, err := i.ParseCredentials(s.ID(), &cc) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(UnknownConnectionValidationError)) } var found bool - var updatedProviders []ProviderCredentialsConfig + var updatedProviders []identity.CredentialsOIDCProvider var updatedIdentifiers []string for _, available := range availableProviders { if p.Unlink == available.Config().ID { for _, link := range cc.Providers { if link.Provider != p.Unlink { - updatedIdentifiers = append(updatedIdentifiers, uid(link.Provider, link.Subject)) + updatedIdentifiers = append(updatedIdentifiers, identity.OIDCUniqueID(link.Provider, link.Subject)) updatedProviders = append(updatedProviders, link) } else { found = true @@ -475,7 +475,7 @@ func (s *Strategy) unlinkProvider(w http.ResponseWriter, r *http.Request, ctxUpd } creds.Identifiers = updatedIdentifiers - creds.Config, err = json.Marshal(&CredentialsConfig{updatedProviders}) + creds.Config, err = json.Marshal(&identity.CredentialsOIDC{updatedProviders}) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(err)) diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index e143dcfae4a6..f809d55009e8 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -212,7 +212,7 @@ func TestSettingsStrategy(t *testing.T) { actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), iid) require.NoError(t, err) - var cc oidc.CredentialsConfig + var cc identity.CredentialsOIDC creds, err := actual.ParseCredentials(identity.CredentialsTypeOIDC, &cc) require.NoError(t, err) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index f40f487b49c1..db5c18b53c04 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -501,7 +501,7 @@ func TestCountActiveCredentials(t *testing.T) { _, reg := internal.NewFastRegistryWithMocks(t) strategy := oidc.NewStrategy(reg) - toJson := func(c oidc.CredentialsConfig) []byte { + toJson := func(c identity.CredentialsOIDC) []byte { out, err := json.Marshal(&c) require.NoError(t, err) return out @@ -520,7 +520,7 @@ func TestCountActiveCredentials(t *testing.T) { { in: identity.CredentialsCollection{{ Type: strategy.ID(), - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ {Subject: "foo", Provider: "bar"}, }}), }}, @@ -529,7 +529,7 @@ func TestCountActiveCredentials(t *testing.T) { in: identity.CredentialsCollection{{ Type: strategy.ID(), Identifiers: []string{""}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ {Subject: "foo", Provider: "bar"}, }}), }}, @@ -538,7 +538,7 @@ func TestCountActiveCredentials(t *testing.T) { in: identity.CredentialsCollection{{ Type: strategy.ID(), Identifiers: []string{"bar:"}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ {Subject: "foo", Provider: "bar"}, }}), }}, @@ -547,7 +547,7 @@ func TestCountActiveCredentials(t *testing.T) { in: identity.CredentialsCollection{{ Type: strategy.ID(), Identifiers: []string{":foo"}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ {Subject: "foo", Provider: "bar"}, }}), }}, @@ -556,7 +556,7 @@ func TestCountActiveCredentials(t *testing.T) { in: identity.CredentialsCollection{{ Type: strategy.ID(), Identifiers: []string{"not-bar:foo"}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ {Subject: "foo", Provider: "bar"}, }}), }}, @@ -565,7 +565,7 @@ func TestCountActiveCredentials(t *testing.T) { in: identity.CredentialsCollection{{ Type: strategy.ID(), Identifiers: []string{"bar:not-foo"}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ {Subject: "foo", Provider: "bar"}, }}), }}, @@ -574,7 +574,7 @@ func TestCountActiveCredentials(t *testing.T) { in: identity.CredentialsCollection{{ Type: strategy.ID(), Identifiers: []string{"bar:foo"}, - Config: toJson(oidc.CredentialsConfig{Providers: []oidc.ProviderCredentialsConfig{ + Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{ {Subject: "foo", Provider: "bar"}, }}), }}, diff --git a/selfservice/strategy/oidc/types.go b/selfservice/strategy/oidc/types.go index 8952e2992a10..12c17c23c946 100644 --- a/selfservice/strategy/oidc/types.go +++ b/selfservice/strategy/oidc/types.go @@ -1,9 +1,6 @@ package oidc import ( - "bytes" - "encoding/json" - "github.com/ory/kratos/text" "github.com/ory/x/stringsx" @@ -12,48 +9,8 @@ import ( "github.com/ory/kratos/ui/node" "github.com/gofrs/uuid" - "github.com/pkg/errors" - - "github.com/ory/kratos/identity" - - "github.com/ory/kratos/x" ) -type CredentialsConfig struct { - Providers []ProviderCredentialsConfig `json:"providers"` -} - -func NewCredentials(idToken, accessToken, refreshToken, provider, subject string) (*identity.Credentials, error) { - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(CredentialsConfig{ - Providers: []ProviderCredentialsConfig{ - { - Subject: subject, - Provider: provider, - InitialIDToken: idToken, - InitialAccessToken: accessToken, - InitialRefreshToken: refreshToken, - }}, - }); err != nil { - return nil, errors.WithStack(x.PseudoPanic. - WithDebugf("Unable to encode password options to JSON: %s", err)) - } - - return &identity.Credentials{ - Type: identity.CredentialsTypeOIDC, - Identifiers: []string{uid(provider, subject)}, - Config: b.Bytes(), - }, nil -} - -type ProviderCredentialsConfig struct { - Subject string `json:"subject"` - Provider string `json:"provider"` - InitialIDToken string `json:"initial_id_token"` - InitialAccessToken string `json:"initial_access_token"` - InitialRefreshToken string `json:"initial_refresh_token"` -} - type FlowMethod struct { *container.Container } diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 641b3fec182a..867560180f47 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -68,7 +68,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) } - var o CredentialsConfig + var o identity.CredentialsPassword d := json.NewDecoder(bytes.NewBuffer(c.Config)) if err := d.Decode(&o); err != nil { return nil, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err) @@ -101,7 +101,7 @@ func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID if err != nil { return err } - co, err := json.Marshal(&CredentialsConfig{HashedPassword: string(hpw)}) + co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(hpw)}) if err != nil { return errors.Wrap(err, "unable to encode password configuration to JSON") } diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index 49b60e089be4..ec12de398945 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -20,8 +20,6 @@ import ( kratos "github.com/ory/kratos-client-go" "github.com/ory/kratos/hash" - "github.com/ory/kratos/selfservice/strategy/password" - "github.com/ory/x/assertx" "github.com/ory/x/errorsx" "github.com/ory/x/ioutilx" @@ -772,7 +770,7 @@ func TestCompleteLogin(t *testing.T) { // check if password hash algorithm is upgraded _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypePassword, identifier) require.NoError(t, err) - var o password.CredentialsConfig + var o identity.CredentialsPassword require.NoError(t, json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o)) assert.True(t, reg.Hasher().Understands([]byte(o.HashedPassword)), "%s", o.HashedPassword) assert.True(t, hash.IsBcryptHash([]byte(o.HashedPassword)), "%s", o.HashedPassword) diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 0916b9fc4255..f449f9831c21 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -114,13 +114,10 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return s.handleRegistrationError(w, r, f, &p, err) } - co, err := json.Marshal(&CredentialsConfig{HashedPassword: string(hpw)}) - if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode password options to JSON: %s", err))) - } - i.Traits = identity.Traits(p.Traits) - i.SetCredentials(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}, Config: co}) + if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, &identity.CredentialsPassword{HashedPassword: string(hpw)}); err != nil { + return s.handleRegistrationError(w, r, f, &p, err) + } if err := s.validateCredentials(r.Context(), i, p.Password); err != nil { return s.handleRegistrationError(w, r, f, &p, err) diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index 45d6aa60d4db..08b5271e6ba5 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -1,7 +1,6 @@ package password import ( - "encoding/json" "net/http" "time" @@ -14,7 +13,6 @@ import ( "github.com/gofrs/uuid" "github.com/pkg/errors" - "github.com/ory/herodot" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" @@ -124,11 +122,6 @@ func (s *Strategy) continueSettingsFlow( return err } - co, err := json.Marshal(&CredentialsConfig{HashedPassword: string(hpw)}) - if err != nil { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode password options to JSON: %s", err)) - } - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) if err != nil { return err @@ -141,8 +134,10 @@ func (s *Strategy) continueSettingsFlow( Identifiers: []string{x.NewUUID().String()}} } - c.Config = co - i.SetCredentials(s.ID(), *c) + if err := i.SetCredentialsWithConfig(s.ID(), *c, &identity.CredentialsPassword{HashedPassword: string(hpw)}); err != nil { + return err + } + if err := s.validateCredentials(r.Context(), i, p.Password); err != nil { return err } diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go index 2b5f2211ee63..8608fbe3e15e 100644 --- a/selfservice/strategy/password/strategy.go +++ b/selfservice/strategy/password/strategy.go @@ -81,7 +81,7 @@ func NewStrategy(d registrationStrategyDependencies) *Strategy { func (s *Strategy) CountActiveCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { for _, c := range cc { if c.Type == s.ID() && len(c.Config) > 0 { - var conf CredentialsConfig + var conf identity.CredentialsPassword if err = json.Unmarshal(c.Config, &conf); err != nil { return 0, errors.WithStack(err) } diff --git a/selfservice/strategy/password/types.go b/selfservice/strategy/password/types.go index e8d8d924310c..4471b2e5e62d 100644 --- a/selfservice/strategy/password/types.go +++ b/selfservice/strategy/password/types.go @@ -4,12 +4,6 @@ import ( "github.com/ory/kratos/ui/container" ) -// CredentialsConfig is the struct that is being used as part of the identity credentials. -type CredentialsConfig struct { - // HashedPassword is a hash-representation of the password. - HashedPassword string `json:"hashed_password"` -} - // submitSelfServiceLoginFlowWithPasswordMethodBody is used to decode the login form payload. // // swagger:model submitSelfServiceLoginFlowWithPasswordMethodBody From e0084ad071399741f79fd677794a537a23a14ca0 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 24 Feb 2022 16:49:18 +0100 Subject: [PATCH 02/15] fix: base64 encode identity schema URLs Previously, identity schema IDs with special characters could lead to broken URLs. This patch introduces a change where identity schema IDs are base64 encoded to address this issue. Schema IDs that are not base64 encoded will continue working. --- schema/handler.go | 7 ++++++- schema/handler_test.go | 11 +++++++++++ schema/schema.go | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/schema/handler.go b/schema/handler.go index 0f60259bbcd5..3639b2147804 100644 --- a/schema/handler.go +++ b/schema/handler.go @@ -90,7 +90,12 @@ func (h *Handler) getByID(w http.ResponseWriter, r *http.Request, ps httprouter. return } - s, err := ss.GetByID(ps.ByName("id")) + id := ps.ByName("id") + if dec, err := base64.RawURLEncoding.DecodeString(ps.ByName("id")); err == nil { + id = string(dec) + } + + s, err := ss.GetByID(id) if err != nil { h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound.WithDebugf("%+v", err))) return diff --git a/schema/handler_test.go b/schema/handler_test.go index 922e4f183812..41afc38d74b6 100644 --- a/schema/handler_test.go +++ b/schema/handler_test.go @@ -61,6 +61,11 @@ func TestHandler(t *testing.T) { URL: urlx.ParseOrPanic("file://./stub"), RawURL: "file://./stub", }, + { + ID: "preset://email", + URL: urlx.ParseOrPanic("file://./stub/identity-2.schema.json"), + RawURL: "file://./stub/identity-2.schema.json", + }, } getSchemaById := func(id string) *schema.Schema { @@ -137,6 +142,12 @@ func TestHandler(t *testing.T) { require.JSONEq(t, string(file), string(server)) }) + t.Run("case=get encoded schema", func(t *testing.T) { + server := getFromTSById("cHJlc2V0Oi8vZW1haWw", http.StatusOK) + file := getFromFS("preset://email") + require.JSONEq(t, string(file), string(server)) + }) + t.Run("case=get unreachable schema", func(t *testing.T) { reason := getFromTSById("unreachable", http.StatusInternalServerError) require.Contains(t, string(reason), "could not be found or opened") diff --git a/schema/schema.go b/schema/schema.go index e5cb2a531d9e..80d93e962070 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -2,6 +2,7 @@ package schema import ( "context" + "encoding/base64" "io/ioutil" "net/url" "strings" @@ -100,5 +101,5 @@ type Schema struct { } func (s *Schema) SchemaURL(host *url.URL) *url.URL { - return urlx.AppendPaths(host, SchemasPath, s.ID) + return urlx.AppendPaths(host, SchemasPath, base64.RawURLEncoding.EncodeToString([]byte(s.ID))) } From 0d4bae9e4b0d36b8e89d56ee156f4e2dbe8e88e8 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 24 Feb 2022 17:21:33 +0100 Subject: [PATCH 03/15] fix: cloud config issue --- contrib/quickstart/kratos/cloud/kratos.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/quickstart/kratos/cloud/kratos.yml b/contrib/quickstart/kratos/cloud/kratos.yml index af6e0eb83fcf..75a1a0e0cc67 100644 --- a/contrib/quickstart/kratos/cloud/kratos.yml +++ b/contrib/quickstart/kratos/cloud/kratos.yml @@ -66,10 +66,10 @@ hashers: cost: 8 identity: - default_schema_id: default + default_schema_id: preset://email schemas: - - id: default - schema: file:///etc/config/kratos/identity.schema.json + - id: preset://email + url: file:///etc/config/kratos/identity.schema.json courier: smtp: From 1cf1520ab062fe9c1a9159d977608f957f6803aa Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 24 Feb 2022 17:24:21 +0100 Subject: [PATCH 04/15] feat: password, social sign, verified email in import This patch introduces the ability to import passwords (cleartext, PKBDF2, Argon2, BCrypt) and Social Sign In connections when creating identities! Closes #605 --- ...o_import_users-with_argon2id_password.json | 17 ++++ ...to_import_users-with_bcrypt2_password.json | 17 ++++ ...eartext_password_and_oidc_credentials.json | 42 +++++++++ ..._to_import_users-with_pkbdf2_password.json | 17 ++++ ..._import_users-without_any_credentials.json | 16 ++++ identity/handler.go | 34 ++++++-- identity/handler_import.go | 86 +++++++++++++++++++ identity/handler_test.go | 77 ++++++++++++++++- selfservice/strategy/password/registration.go | 6 ++ selfservice/strategy/password/settings.go | 4 +- 10 files changed, 300 insertions(+), 16 deletions(-) create mode 100644 identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_argon2id_password.json create mode 100644 identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_bcrypt2_password.json create mode 100644 identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json create mode 100644 identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_pkbdf2_password.json create mode 100644 identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_any_credentials.json create mode 100644 identity/handler_import.go diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_argon2id_password.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_argon2id_password.json new file mode 100644 index 000000000000..cf7eaa05345f --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_argon2id_password.json @@ -0,0 +1,17 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-5@ory.sh" + ], + "config": { + } + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-5@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_bcrypt2_password.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_bcrypt2_password.json new file mode 100644 index 000000000000..32bd0337b665 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_bcrypt2_password.json @@ -0,0 +1,17 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-4@ory.sh" + ], + "config": { + } + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-4@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json new file mode 100644 index 000000000000..b13a4c7f06bd --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json @@ -0,0 +1,42 @@ +{ + "credentials": { + "oidc": { + "type": "oidc", + "identifiers": [ + "google:import-2", + "github:import-2" + ], + "config": { + "providers": [ + { + "subject": "import-2", + "provider": "google", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + }, + { + "subject": "import-2", + "provider": "github", + "initial_id_token": "", + "initial_access_token": "", + "initial_refresh_token": "" + } + ] + } + }, + "password": { + "type": "password", + "identifiers": [ + "import-2@ory.sh" + ], + "config": { + } + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-2@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_pkbdf2_password.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_pkbdf2_password.json new file mode 100644 index 000000000000..9de95347dde9 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_pkbdf2_password.json @@ -0,0 +1,17 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-3@ory.sh" + ], + "config": { + } + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-3@ory.sh" + } +} diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_any_credentials.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_any_credentials.json new file mode 100644 index 000000000000..a8cf8009ba7e --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-without_any_credentials.json @@ -0,0 +1,16 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-1@ory.sh" + ], + "config": {} + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-1@ory.sh" + } +} diff --git a/identity/handler.go b/identity/handler.go index bd9d4f3dd1e2..d5e71a0f6ebc 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -3,6 +3,7 @@ package identity import ( "context" "encoding/json" + "github.com/ory/kratos/hash" "net/http" "time" @@ -34,6 +35,7 @@ type ( config.Provider x.CSRFProvider cipher.Provider + hash.HashProvider } HandlerProvider interface { IdentityHandler() *Handler @@ -236,7 +238,7 @@ type AdminIdentityImportCredentials struct { OIDC *AdminIdentityImportCredentialsOIDC `json:"oidc,omitempty"` } -// swagger:model AdminCreateIdentityImportCredentialsPassword +// swagger:model adminCreateIdentityImportCredentialsPassword type AdminIdentityImportCredentialsPassword struct { // The hashed password in [PHC format]( https://www.ory.sh/docs/kratos/concepts/credentials/username-email-password#hashed-password-format) HashedPassword string `json:"hashed_password"` @@ -245,12 +247,22 @@ type AdminIdentityImportCredentialsPassword struct { Password string `json:"password"` } -// swagger:model AdminCreateIdentityImportCredentialsOIDC +// swagger:model adminCreateIdentityImportCredentialsOidc type AdminIdentityImportCredentialsOIDC struct { + // A list of OpenID Connect Providers + Providers []AdminCreateIdentityImportCredentialsOidcProvider `json:"providers"` +} + +// swagger:model adminCreateIdentityImportCredentialsOidcProvider +type AdminCreateIdentityImportCredentialsOidcProvider struct { // The subject (`sub`) of the OpenID Connect connection. Usually the `sub` field of the ID Token. + // + // required: true Subject string `json:"subject"` // The OpenID Connect provider to link the subject to. Usually something like `google` or `github`. + // + // required: true Provider string `json:"provider"` } @@ -297,16 +309,18 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa } i := &Identity{ - SchemaID: cr.SchemaID, - Traits: []byte(cr.Traits), - State: state, - StateChangedAt: &stateChangedAt, - //Credentials: cr.Credentials, + SchemaID: cr.SchemaID, + Traits: []byte(cr.Traits), + State: state, + StateChangedAt: &stateChangedAt, VerifiableAddresses: cr.VerifiableAddresses, RecoveryAddresses: cr.RecoveryAddresses, } - //i.Traits = identity.Traits(p.Traits) - //i.SetCredentials(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}, Config: co}) + + if err := h.importCredentials(r.Context(), i, cr.Credentials); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } if err := h.r.IdentityManager().Create(r.Context(), i); err != nil { h.r.Writer().WriteError(w, r, err) @@ -339,6 +353,8 @@ type adminUpdateIdentity struct { type AdminUpdateIdentityBody struct { // SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set // will update the Identity's SchemaID. + // + // required: true SchemaID string `json:"schema_id"` // Traits represent an identity's traits. The identity is able to create, modify, and delete traits diff --git a/identity/handler_import.go b/identity/handler_import.go new file mode 100644 index 000000000000..2aed1f5c382c --- /dev/null +++ b/identity/handler_import.go @@ -0,0 +1,86 @@ +package identity + +import ( + "context" + "encoding/json" + "github.com/ory/herodot" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/x" + "github.com/pkg/errors" +) + +func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *AdminIdentityImportCredentials) error { + if creds == nil { + return nil + } + + if creds.Password != nil { + if err := h.importPasswordCredentials(ctx, i, creds.Password); err != nil { + return err + } + } + + if creds.OIDC != nil { + if err := h.importOIDCCredentials(ctx, i, creds.OIDC); err != nil { + return err + } + } + + return nil +} + +func (h *Handler) importPasswordCredentials(ctx context.Context, i *Identity, creds *AdminIdentityImportCredentialsPassword) (err error) { + // In here we deliberately ignore any password policies as the point here is to import passwords, even if they + // are not matching the policy, as the user needs to able to sign in with their old password. + hashed := []byte(creds.HashedPassword) + if len(creds.Password) > 0 { + // Importing a clear text password + hashed, err = h.r.Hasher().Generate(ctx, []byte(creds.Password)) + if err != nil { + return err + } + + creds.HashedPassword = string(hashed) + } + + if !(hash.IsArgon2idHash(hashed) || hash.IsBcryptHash(hashed) || hash.IsPbkdf2Hash(hashed)) { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("The imported password does not match any known hash format. For more information see https://www.ory.sh/dr/2")) + } + + return i.SetCredentialsWithConfig(CredentialsTypePassword, Credentials{}, CredentialsPassword{HashedPassword: string(hashed)}) +} + +func (h *Handler) importOIDCCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsOIDC) error { + var target CredentialsOIDC + c, ok := i.GetCredentials(CredentialsTypeOIDC) + if !ok { + var providers []CredentialsOIDCProvider + var ids []string + for _, p := range creds.Providers { + ids = append(ids, OIDCUniqueID(p.Provider, p.Subject)) + providers = append(providers, CredentialsOIDCProvider{ + Subject: p.Subject, + Provider: p.Provider, + }) + } + + return i.SetCredentialsWithConfig( + CredentialsTypeOIDC, + Credentials{Identifiers: ids}, + CredentialsOIDC{Providers: providers}, + ) + } + + if err := json.Unmarshal(c.Config, &target); err != nil { + return errors.WithStack(x.PseudoPanic.WithWrap(err)) + } + + for _, p := range creds.Providers { + c.Identifiers = append(c.Identifiers, OIDCUniqueID(p.Provider, p.Subject)) + target.Providers = append(target.Providers, CredentialsOIDCProvider{ + Subject: p.Subject, + Provider: p.Provider, + }) + } + return i.SetCredentialsWithConfig(CredentialsTypeOIDC, *c, &target) +} diff --git a/identity/handler_test.go b/identity/handler_test.go index 03d5cc7ef0c8..bd54fc1a0834 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -5,6 +5,9 @@ import ( "context" "encoding/json" "fmt" + "github.com/gofrs/uuid" + "github.com/ory/kratos/hash" + "github.com/ory/x/snapshotx" "io/ioutil" "net/http" "net/http/httptest" @@ -93,17 +96,14 @@ func TestHandler(t *testing.T) { }) t.Run("case=should return 404 on a non-existing resource", func(t *testing.T) { - for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { _ = get(t, ts, "/identities/does-not-exist", http.StatusNotFound) - }) } }) t.Run("case=should fail to create an identity because schema id does not exist", func(t *testing.T) { - for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { var i identity.AdminCreateIdentityBody @@ -122,7 +122,6 @@ func TestHandler(t *testing.T) { i.Traits = []byte(`{"bar":123}`) res := send(t, ts, "POST", "/identities", http.StatusBadRequest, &i) assert.Contains(t, res.Get("error.reason").String(), "I[#/traits/bar] S[#/properties/traits/properties/bar/type] expected string, but got number") - }) } }) @@ -150,6 +149,76 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=should be able to import users", func(t *testing.T) { + ignoreDefault := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at"} + t.Run("without any credentials", func(t *testing.T) { + res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-1@ory.sh"}`)}) + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) + require.NoError(t, err) + + snapshotx.SnapshotTExceptMatchingKeys(t, identity.WithCredentialsInJSON(*actual), ignoreDefault) + }) + + t.Run("with cleartext password and oidc credentials", func(t *testing.T) { + res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-2@ory.sh"}`), + Credentials: &identity.AdminIdentityImportCredentials{ + Password: &identity.AdminIdentityImportCredentialsPassword{ + Password: "123456", + }, + OIDC: &identity.AdminIdentityImportCredentialsOIDC{ + Providers: []identity.AdminCreateIdentityImportCredentialsOidcProvider{ + {Subject: "import-2", Provider: "google"}, + {Subject: "import-2", Provider: "github"}, + }, + }, + }, + }) + + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) + require.NoError(t, err) + + snapshotx.SnapshotTExceptMatchingKeys(t, identity.WithCredentialsInJSON(*actual), append(ignoreDefault, "hashed_password")) + + require.NoError(t, hash.Compare(ctx, []byte("123456"), []byte(gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "hashed_password").String()))) + }) + + t.Run("with pkbdf2 password", func(t *testing.T) { + res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-3@ory.sh"}`), + Credentials: &identity.AdminIdentityImportCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{ + HashedPassword: "$pbkdf2-sha256$i=1000,l=128$e8/arsEf4cvQihdNgqj0Nw$5xQQKNTyeTHx2Ld5/JDE7A"}}}) + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) + require.NoError(t, err) + + snapshotx.SnapshotTExceptMatchingKeys(t, identity.WithCredentialsInJSON(*actual), append(ignoreDefault, "hashed_password")) + + require.NoError(t, hash.Compare(ctx, []byte("123456"), []byte(gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "hashed_password").String()))) + }) + + t.Run("with bcrypt2 password", func(t *testing.T) { + res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-4@ory.sh"}`), + Credentials: &identity.AdminIdentityImportCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{ + HashedPassword: "$2a$10$ZsCsoVQ3xfBG/K2z2XpBf.tm90GZmtOqtqWcB5.pYd5Eq8y7RlDyq"}}}) + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) + require.NoError(t, err) + + snapshotx.SnapshotTExceptMatchingKeys(t, identity.WithCredentialsInJSON(*actual), append(ignoreDefault, "hashed_password")) + + require.NoError(t, hash.Compare(ctx, []byte("123456"), []byte(gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "hashed_password").String()))) + }) + + t.Run("with argon2id password", func(t *testing.T) { + res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-5@ory.sh"}`), + Credentials: &identity.AdminIdentityImportCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{ + HashedPassword: "$argon2id$v=19$m=16,t=2,p=1$bVI1aE1SaTV6SGQ3bzdXdw$fnjCcZYmEPOUOjYXsT92Cg"}}}) + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) + require.NoError(t, err) + + snapshotx.SnapshotTExceptMatchingKeys(t, identity.WithCredentialsInJSON(*actual), append(ignoreDefault, "hashed_password")) + + require.NoError(t, hash.Compare(ctx, []byte("123456"), []byte(gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "hashed_password").String()))) + }) + }) + t.Run("case=unable to set ID itself", func(t *testing.T) { for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index f449f9831c21..bc4e90ed67d4 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -139,6 +139,12 @@ func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity return schema.NewMissingIdentifierError() } + // Sometimes, no identifier is set, but we still want to validate the password! + ids := c.Identifiers + if len(ids) == 0 { + ids = []string{""} + } + for _, id := range c.Identifiers { if err := s.d.PasswordValidator().Validate(ctx, id, pw); err != nil { if _, ok := errorsx.Cause(err).(*herodot.DefaultError); ok { diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index 08b5271e6ba5..ee0c1f3e41dd 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -129,9 +129,7 @@ func (s *Strategy) continueSettingsFlow( c, ok := i.GetCredentials(s.ID()) if !ok { - c = &identity.Credentials{Type: s.ID(), - // We need to insert a random identifier now... - Identifiers: []string{x.NewUUID().String()}} + c = &identity.Credentials{Type: s.ID()} } if err := i.SetCredentialsWithConfig(s.ID(), *c, &identity.CredentialsPassword{HashedPassword: string(hpw)}); err != nil { From 113168b17e090ee6e37669e245358dad7006f820 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 24 Feb 2022 17:27:29 +0100 Subject: [PATCH 05/15] fix: return 400 instead of 404 on admin recovery Closes #1664 --- selfservice/strategy/link/strategy_recovery.go | 8 ++++++-- selfservice/strategy/link/strategy_recovery_test.go | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 6bc97c988709..33ba3cd81887 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -121,8 +121,8 @@ type selfServiceRecoveryLink struct { // // Responses: // 200: selfServiceRecoveryLink -// 404: jsonError // 400: jsonError +// 404: jsonError // 500: jsonError func (s *Strategy) createRecoveryLink(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { var p adminCreateSelfServiceRecoveryLinkBody @@ -159,10 +159,14 @@ func (s *Strategy) createRecoveryLink(w http.ResponseWriter, r *http.Request, _ } id, err := s.d.IdentityPool().GetIdentity(r.Context(), p.IdentityID) - if err != nil { + if errors.Is(err, herodot.ErrNotFound) { + s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("The requested identity id does not exist.").WithWrap(err))) + return + } else if err != nil { s.d.Writer().WriteError(w, r, err) return } + token := NewRecoveryToken(id.ID, expiresIn) if err := s.d.RecoveryTokenPersister().CreateRecoveryToken(r.Context(), token); err != nil { s.d.Writer().WriteError(w, r, err) diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index c3b1935e2c1c..8cb194afd986 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -93,7 +93,7 @@ func TestAdminStrategy(t *testing.T) { IdentityId: x.NewUUID().String(), }).Execute() require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) - assert.EqualError(t, err.(*kratos.GenericOpenAPIError), "404 Not Found") + assert.EqualError(t, err.(*kratos.GenericOpenAPIError), "400 Bad Request") }) t.Run("description=should create a valid recovery link without email", func(t *testing.T) { From 42b62fec20de388ebdf46009cec292ec3ce94a49 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 24 Feb 2022 17:28:07 +0100 Subject: [PATCH 06/15] chore: format --- identity/handler.go | 3 ++- identity/handler_import.go | 4 +++- identity/handler_test.go | 8 +++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index d5e71a0f6ebc..c09ad6a82822 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -3,10 +3,11 @@ package identity import ( "context" "encoding/json" - "github.com/ory/kratos/hash" "net/http" "time" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/x" "github.com/ory/kratos/cipher" diff --git a/identity/handler_import.go b/identity/handler_import.go index 2aed1f5c382c..e6007275bbe5 100644 --- a/identity/handler_import.go +++ b/identity/handler_import.go @@ -3,10 +3,12 @@ package identity import ( "context" "encoding/json" + + "github.com/pkg/errors" + "github.com/ory/herodot" "github.com/ory/kratos/hash" "github.com/ory/kratos/x" - "github.com/pkg/errors" ) func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *AdminIdentityImportCredentials) error { diff --git a/identity/handler_test.go b/identity/handler_test.go index bd54fc1a0834..1681c313634b 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -5,15 +5,17 @@ import ( "context" "encoding/json" "fmt" - "github.com/gofrs/uuid" - "github.com/ory/kratos/hash" - "github.com/ory/x/snapshotx" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" + "github.com/gofrs/uuid" + + "github.com/ory/kratos/hash" + "github.com/ory/x/snapshotx" + "github.com/bxcodec/faker/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" From 38718ca3a06d367fc9c2fb97d31b0686910ed960 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 11:06:04 +0100 Subject: [PATCH 07/15] refactor: mimic credentials config on import --- identity/handler.go | 18 ++++++++++++++++-- identity/handler_import.go | 12 ++++++------ identity/handler_test.go | 26 +++++++++++++++----------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index c09ad6a82822..301d5564d9fc 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -233,14 +233,20 @@ type AdminCreateIdentityBody struct { // swagger:model adminIdentityImportCredentials type AdminIdentityImportCredentials struct { // Password if set will import a password credential. - Password *AdminIdentityImportCredentialsPassword `json:"password,omitempty"` + Password *AdminIdentityImportCredentialsPassword `json:"password"` // OIDC if set will import an OIDC credential. - OIDC *AdminIdentityImportCredentialsOIDC `json:"oidc,omitempty"` + OIDC *AdminIdentityImportCredentialsOIDC `json:"oidc"` } // swagger:model adminCreateIdentityImportCredentialsPassword type AdminIdentityImportCredentialsPassword struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsPasswordConfig `json:"config"` +} + +// swagger:model adminCreateIdentityImportCredentialsPasswordConfig +type AdminIdentityImportCredentialsPasswordConfig struct { // The hashed password in [PHC format]( https://www.ory.sh/docs/kratos/concepts/credentials/username-email-password#hashed-password-format) HashedPassword string `json:"hashed_password"` @@ -250,6 +256,14 @@ type AdminIdentityImportCredentialsPassword struct { // swagger:model adminCreateIdentityImportCredentialsOidc type AdminIdentityImportCredentialsOIDC struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsOIDCConfig `json:"config"` +} + +// swagger:model adminCreateIdentityImportCredentialsOidcConfig +type AdminIdentityImportCredentialsOIDCConfig struct { + // Configuration options for the import. + Config AdminIdentityImportCredentialsPasswordConfig `json:"config"` // A list of OpenID Connect Providers Providers []AdminCreateIdentityImportCredentialsOidcProvider `json:"providers"` } diff --git a/identity/handler_import.go b/identity/handler_import.go index e6007275bbe5..e0835f6de710 100644 --- a/identity/handler_import.go +++ b/identity/handler_import.go @@ -34,15 +34,15 @@ func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *Adm func (h *Handler) importPasswordCredentials(ctx context.Context, i *Identity, creds *AdminIdentityImportCredentialsPassword) (err error) { // In here we deliberately ignore any password policies as the point here is to import passwords, even if they // are not matching the policy, as the user needs to able to sign in with their old password. - hashed := []byte(creds.HashedPassword) - if len(creds.Password) > 0 { + hashed := []byte(creds.Config.HashedPassword) + if len(creds.Config.Password) > 0 { // Importing a clear text password - hashed, err = h.r.Hasher().Generate(ctx, []byte(creds.Password)) + hashed, err = h.r.Hasher().Generate(ctx, []byte(creds.Config.Password)) if err != nil { return err } - creds.HashedPassword = string(hashed) + creds.Config.HashedPassword = string(hashed) } if !(hash.IsArgon2idHash(hashed) || hash.IsBcryptHash(hashed) || hash.IsPbkdf2Hash(hashed)) { @@ -58,7 +58,7 @@ func (h *Handler) importOIDCCredentials(_ context.Context, i *Identity, creds *A if !ok { var providers []CredentialsOIDCProvider var ids []string - for _, p := range creds.Providers { + for _, p := range creds.Config.Providers { ids = append(ids, OIDCUniqueID(p.Provider, p.Subject)) providers = append(providers, CredentialsOIDCProvider{ Subject: p.Subject, @@ -77,7 +77,7 @@ func (h *Handler) importOIDCCredentials(_ context.Context, i *Identity, creds *A return errors.WithStack(x.PseudoPanic.WithWrap(err)) } - for _, p := range creds.Providers { + for _, p := range creds.Config.Providers { c.Identifiers = append(c.Identifiers, OIDCUniqueID(p.Provider, p.Subject)) target.Providers = append(target.Providers, CredentialsOIDCProvider{ Subject: p.Subject, diff --git a/identity/handler_test.go b/identity/handler_test.go index 1681c313634b..32b7b7bcd26d 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -165,12 +165,16 @@ func TestHandler(t *testing.T) { res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-2@ory.sh"}`), Credentials: &identity.AdminIdentityImportCredentials{ Password: &identity.AdminIdentityImportCredentialsPassword{ - Password: "123456", + Config: identity.AdminIdentityImportCredentialsPasswordConfig{ + Password: "123456", + }, }, OIDC: &identity.AdminIdentityImportCredentialsOIDC{ - Providers: []identity.AdminCreateIdentityImportCredentialsOidcProvider{ - {Subject: "import-2", Provider: "google"}, - {Subject: "import-2", Provider: "github"}, + Config: identity.AdminIdentityImportCredentialsOIDCConfig{ + Providers: []identity.AdminCreateIdentityImportCredentialsOidcProvider{ + {Subject: "import-2", Provider: "google"}, + {Subject: "import-2", Provider: "github"}, + }, }, }, }, @@ -187,7 +191,7 @@ func TestHandler(t *testing.T) { t.Run("with pkbdf2 password", func(t *testing.T) { res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-3@ory.sh"}`), Credentials: &identity.AdminIdentityImportCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{ - HashedPassword: "$pbkdf2-sha256$i=1000,l=128$e8/arsEf4cvQihdNgqj0Nw$5xQQKNTyeTHx2Ld5/JDE7A"}}}) + Config: identity.AdminIdentityImportCredentialsPasswordConfig{HashedPassword: "$pbkdf2-sha256$i=1000,l=128$e8/arsEf4cvQihdNgqj0Nw$5xQQKNTyeTHx2Ld5/JDE7A"}}}}) actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) require.NoError(t, err) @@ -199,7 +203,7 @@ func TestHandler(t *testing.T) { t.Run("with bcrypt2 password", func(t *testing.T) { res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-4@ory.sh"}`), Credentials: &identity.AdminIdentityImportCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{ - HashedPassword: "$2a$10$ZsCsoVQ3xfBG/K2z2XpBf.tm90GZmtOqtqWcB5.pYd5Eq8y7RlDyq"}}}) + Config: identity.AdminIdentityImportCredentialsPasswordConfig{HashedPassword: "$2a$10$ZsCsoVQ3xfBG/K2z2XpBf.tm90GZmtOqtqWcB5.pYd5Eq8y7RlDyq"}}}}) actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) require.NoError(t, err) @@ -211,7 +215,7 @@ func TestHandler(t *testing.T) { t.Run("with argon2id password", func(t *testing.T) { res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "import-5@ory.sh"}`), Credentials: &identity.AdminIdentityImportCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{ - HashedPassword: "$argon2id$v=19$m=16,t=2,p=1$bVI1aE1SaTV6SGQ3bzdXdw$fnjCcZYmEPOUOjYXsT92Cg"}}}) + Config: identity.AdminIdentityImportCredentialsPasswordConfig{HashedPassword: "$argon2id$v=19$m=16,t=2,p=1$bVI1aE1SaTV6SGQ3bzdXdw$fnjCcZYmEPOUOjYXsT92Cg"}}}}) actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) require.NoError(t, err) @@ -491,7 +495,7 @@ func TestHandler(t *testing.T) { assert.JSONEq(t, string(cr.Traits), res.Get("traits").Raw, "%s", res.Raw) assert.EqualValues(t, "employee", res.Get("schema_id").String(), "%s", res.Raw) assert.EqualValues(t, identity.StateActive, res.Get("state").String(), "%s", res.Raw) - assert.EqualValues(t, mockServerURL.String()+"/schemas/employee", res.Get("schema_url").String(), "%s", res.Raw) + assert.EqualValues(t, mockServerURL.String()+"/schemas/ZW1wbG95ZWU", res.Get("schema_url").String(), "%s", res.Raw) }) } }) @@ -508,7 +512,7 @@ func TestHandler(t *testing.T) { assert.JSONEq(t, string(cr.Traits), res.Get("traits").Raw, "%s", res.Raw) assert.EqualValues(t, "employee", res.Get("schema_id").String(), "%s", res.Raw) assert.EqualValues(t, identity.StateActive, res.Get("state").String(), "%s", res.Raw) - assert.EqualValues(t, mockServerURL.String()+"/schemas/employee", res.Get("schema_url").String(), "%s", res.Raw) + assert.EqualValues(t, mockServerURL.String()+"/schemas/ZW1wbG95ZWU", res.Get("schema_url").String(), "%s", res.Raw) }) } }) @@ -525,7 +529,7 @@ func TestHandler(t *testing.T) { assert.JSONEq(t, string(cr.Traits), res.Get("traits").Raw, "%s", res.Raw) assert.EqualValues(t, "employee", res.Get("schema_id").String(), "%s", res.Raw) assert.EqualValues(t, identity.StateInactive, res.Get("state").String(), "%s", res.Raw) - assert.EqualValues(t, mockServerURL.String()+"/schemas/employee", res.Get("schema_url").String(), "%s", res.Raw) + assert.EqualValues(t, mockServerURL.String()+"/schemas/ZW1wbG95ZWU", res.Get("schema_url").String(), "%s", res.Raw) }) } }) @@ -548,7 +552,7 @@ func TestHandler(t *testing.T) { }) assert.EqualValues(t, "employee", res.Get("schema_id").String(), "%s", res.Raw) - assert.EqualValues(t, mockServerURL.String()+"/schemas/employee", res.Get("schema_url").String(), "%s", res.Raw) + assert.EqualValues(t, mockServerURL.String()+"/schemas/ZW1wbG95ZWU", res.Get("schema_url").String(), "%s", res.Raw) assert.EqualValues(t, updatedEmail, res.Get("traits.email").String(), "%s", res.Raw) assert.EqualValues(t, "ory", res.Get("traits.department").String(), "%s", res.Raw) assert.EqualValues(t, updatedEmail, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) From ad8ba1676e17fea390a4b1849fca5812c3f81ef8 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 11:06:11 +0100 Subject: [PATCH 08/15] test(e2e): add import tests --- .../profiles/import/import.spec.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 test/e2e/cypress/integration/profiles/import/import.spec.ts diff --git a/test/e2e/cypress/integration/profiles/import/import.spec.ts b/test/e2e/cypress/integration/profiles/import/import.spec.ts new file mode 100644 index 000000000000..366a225e309a --- /dev/null +++ b/test/e2e/cypress/integration/profiles/import/import.spec.ts @@ -0,0 +1,126 @@ +import { routes as express } from '../../../helpers/express' +import { gen, KRATOS_ADMIN, website } from '../../../helpers' + +context('Import Identities', () => { + before(() => { + cy.useConfigProfile('oidc') + cy.proxy('express') + }) + + beforeEach(() => { + cy.clearAllCookies() + }) + + const password = gen.password() + for (const tc of [ + { + name: 'cleartext', + config: { + password + }, + checkPassword: password + }, + { + name: 'pbkdf2', + config: { + hashed_password: + '$pbkdf2-sha256$i=1000,l=128$e8/arsEf4cvQihdNgqj0Nw$5xQQKNTyeTHx2Ld5/JDE7A' + }, + checkPassword: '123456' + }, + { + name: 'bcrypt', + config: { + hashed_password: + '$2a$10$ZsCsoVQ3xfBG/K2z2XpBf.tm90GZmtOqtqWcB5.pYd5Eq8y7RlDyq' + }, + checkPassword: '123456' + }, + { + name: 'argon2id', + config: { + hashed_password: + '$argon2id$v=19$m=16,t=2,p=1$bVI1aE1SaTV6SGQ3bzdXdw$fnjCcZYmEPOUOjYXsT92Cg' + }, + checkPassword: '123456' + } + ]) { + it(`should be able to sign in using an imported password (${tc.name})`, () => { + const email = gen.email() + cy.request('POST', `${KRATOS_ADMIN}/identities`, { + schema_id: 'default', + traits: { + email, + website + }, + credentials: { + password: { + config: tc.config + } + } + }) + + cy.visit(express.login) + + // Try to sign in with an incorrect password + cy.get('input[name="password_identifier"]').type(email) + cy.get('input[name="password"]').type('invalid-password') + cy.submitPasswordForm() + cy.get('*[data-testid="ui/message/4000006"]').should( + 'contain.text', + 'credentials are invalid' + ) + + // But with correct password it succeeds + cy.get('input[name="password"]').type(tc.checkPassword) + cy.submitPasswordForm() + + cy.location('pathname').should('not.contain', '/login') + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.website).to.equal(website) + }) + }) + } + + it.only(`should be able to sign in using imported oidc credentials`, () => { + const email = gen.email() + const website = 'https://' + gen.password() + '.com' + cy.request('POST', `${KRATOS_ADMIN}/identities`, { + schema_id: 'default', + traits: { + email, + website + }, + credentials: { + oidc: { + config: { + providers: [ + { + provider: 'hydra', + subject: email + } + ] + } + } + } + }) + + cy.visit(express.login) + cy.triggerOidc({ url: express.login }) + + cy.get('#username').clear().type(email) + cy.get('#remember').click() + cy.get('#accept').click() + cy.get('[name="scope"]').each(($el) => cy.wrap($el).click()) + cy.get('#remember').click() + cy.get('#accept').click() + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.website).to.equal(website) + }) + }) +}) From 31b71d24d2901dfdd222fe98127bed65bad19b25 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 12:59:32 +0100 Subject: [PATCH 09/15] fix: resovle lint errors --- Makefile | 2 +- selfservice/strategy/oidc/strategy_settings.go | 2 +- selfservice/strategy/password/registration.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index bdabbea95f70..0359a659c7c3 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ node_modules: package.json Makefile .bin/golangci-lint: Makefile - bash <(curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh) -d -b .bin v1.28.3 + bash <(curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh) -d -b .bin v1.44.2 .bin/hydra: Makefile bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v1.11.0 diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index e9232568befc..df7eaaab383b 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -475,7 +475,7 @@ func (s *Strategy) unlinkProvider(w http.ResponseWriter, r *http.Request, ctxUpd } creds.Identifiers = updatedIdentifiers - creds.Config, err = json.Marshal(&identity.CredentialsOIDC{updatedProviders}) + creds.Config, err = json.Marshal(&identity.CredentialsOIDC{Providers: updatedProviders}) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(err)) diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index bc4e90ed67d4..c3195a1305d7 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -145,7 +145,7 @@ func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity ids = []string{""} } - for _, id := range c.Identifiers { + for _, id := range ids { if err := s.d.PasswordValidator().Validate(ctx, id, pw); err != nil { if _, ok := errorsx.Cause(err).(*herodot.DefaultError); ok { return err From ea7e825ebf1e99f6bc4fcf961efd0620bba9a3b4 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 13:47:10 +0100 Subject: [PATCH 10/15] fix: improve password error resilience on settings flow --- selfservice/strategy/password/registration.go | 16 +++++------ selfservice/strategy/password/settings.go | 1 - .../strategy/password/settings_test.go | 28 ++++++++++++++----- selfservice/strategy/password/validator.go | 2 +- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index c3195a1305d7..b94270df9507 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -136,16 +136,16 @@ func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity // This should never happen return errors.WithStack(x.PseudoPanic.WithReasonf("identity object did not provide the %s CredentialType unexpectedly", identity.CredentialsTypePassword)) } else if len(c.Identifiers) == 0 { - return schema.NewMissingIdentifierError() - } - - // Sometimes, no identifier is set, but we still want to validate the password! - ids := c.Identifiers - if len(ids) == 0 { - ids = []string{""} + if err := s.d.PasswordValidator().Validate(ctx, "", pw); err != nil { + if _, ok := errorsx.Cause(err).(*herodot.DefaultError); ok { + return err + } + return schema.NewPasswordPolicyViolationError("#/password", err.Error()) + } + return nil } - for _, id := range ids { + for _, id := range c.Identifiers { if err := s.d.PasswordValidator().Validate(ctx, id, pw); err != nil { if _, ok := errorsx.Cause(err).(*herodot.DefaultError); ok { return err diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index ee0c1f3e41dd..9b9018a9f834 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -141,7 +141,6 @@ func (s *Strategy) continueSettingsFlow( } ctxUpdate.UpdateIdentity(i) - return nil } diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go index 5097798ae0f1..ea7c151943e2 100644 --- a/selfservice/strategy/password/settings_test.go +++ b/selfservice/strategy/password/settings_test.go @@ -59,6 +59,15 @@ func newEmptyIdentity() *identity.Identity { } } +func newIdentityWithoutCredentials(email string) *identity.Identity { + return &identity.Identity{ + ID: x.NewUUID(), + State: identity.StateActive, + Traits: identity.Traits(`{"email":"` + email + `"}`), + SchemaID: config.DefaultIdentityTraitsSchemaID, + } +} + func TestSettings(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh/") @@ -321,6 +330,7 @@ func TestSettings(t *testing.T) { } t.Run("description=should update the password even if no password was set before", func(t *testing.T) { + email := fmt.Sprintf("test+%s@ory.sh", x.NewUUID()) var check = func(t *testing.T, actual string, id *identity.Identity) { assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(name==password).attributes.value").String(), "%s", actual) @@ -330,7 +340,7 @@ func TestSettings(t *testing.T) { cfg := string(actualIdentity.Credentials[identity.CredentialsTypePassword].Config) assert.Contains(t, cfg, "hashed_password", "%+v", actualIdentity.Credentials) require.Len(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers, 1) - assert.Contains(t, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers[0], "-4") + assert.Equal(t, email, actualIdentity.Credentials[identity.CredentialsTypePassword].Identifiers[0]) } var payload = func(v url.Values) { @@ -338,19 +348,23 @@ func TestSettings(t *testing.T) { v.Set("password", randx.MustString(16, randx.AlphaNum)) } + id := newIdentityWithoutCredentials(email) + browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + t.Run("type=api", func(t *testing.T) { - actual := expectSuccess(t, true, false, apiUser2, payload) - check(t, actual, apiIdentity2) + actual := expectSuccess(t, true, false, apiUser, payload) + check(t, actual, id) }) t.Run("type=spa", func(t *testing.T) { - actual := expectSuccess(t, false, true, browserUser2, payload) - check(t, actual, browserIdentity2) + actual := expectSuccess(t, false, true, browserUser, payload) + check(t, actual, id) }) t.Run("type=browser", func(t *testing.T) { - actual := expectSuccess(t, false, false, browserUser2, payload) - check(t, actual, browserIdentity2) + actual := expectSuccess(t, false, false, browserUser, payload) + check(t, actual, id) }) }) diff --git a/selfservice/strategy/password/validator.go b/selfservice/strategy/password/validator.go index 09d02960dd71..ad8088b05801 100644 --- a/selfservice/strategy/password/validator.go +++ b/selfservice/strategy/password/validator.go @@ -149,7 +149,7 @@ func (s *DefaultPasswordValidator) Validate(ctx context.Context, identifier, pas return errors.Errorf("password length must be at least %d characters but only got %d", passwordPolicyConfig.MinPasswordLength, len(password)) } - if passwordPolicyConfig.IdentifierSimilarityCheckEnabled { + if passwordPolicyConfig.IdentifierSimilarityCheckEnabled && len(identifier) > 0 { compIdentifier, compPassword := strings.ToLower(identifier), strings.ToLower(password) dist := levenshtein.Distance(compIdentifier, compPassword) lcs := float32(lcsLength(compIdentifier, compPassword)) / float32(len(compPassword)) From 59ff162ee10f7ebc7413a8d5be82bb95d86241e5 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 13:47:26 +0100 Subject: [PATCH 11/15] test: update fixtures --- .../196d8c1e-4f04-40f0-94b3-5ec43996b28a.json | 2 +- .../2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json | 2 +- .../359963ec-b09b-4ea0-aece-fb4dd95f304a.json | 2 +- .../5ff66179-c240-4703-b0d8-494592cefff5.json | 2 +- .../a251ebc2-880c-4f76-a8f3-38e6940eab0e.json | 2 +- .../d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json | 2 +- .../ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json | 2 +- .../7458af86-c1d8-401c-978a-8da89133f78b.json | 2 +- .../8571e374-38f2-4f46-8ad3-b9d914e174d3.json | 2 +- .../dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json | 2 +- .../f38cdebe-e567-42c9-a562-1bd4dee40998.json | 2 +- .../194c5b05-0487-4a11-bcbc-f301c9ff9678.json | 2 +- .../19ede218-928c-4e02-ab49-b76e12b34f31.json | 2 +- .../21c5f714-3089-49d2-b387-f244d4dd9e00.json | 2 +- .../74fd6c53-7651-453e-90b8-2c5adbf911bb.json | 2 +- .../77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json | 2 +- .../8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json | 2 +- .../90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json | 2 +- .../a79bfcf1-68ae-49de-8b23-4f96921b8341.json | 2 +- .../aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json | 2 +- .../cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json | 2 +- schema/handler.go | 16 ++++++++++------ 22 files changed, 31 insertions(+), 27 deletions(-) diff --git a/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json b/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json index 2d6996ff332d..b7f4f31b6adf 100644 --- a/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json +++ b/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json @@ -1,7 +1,7 @@ { "id": "196d8c1e-4f04-40f0-94b3-5ec43996b28a", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/identity/2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json b/persistence/sql/migratest/fixtures/identity/2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json index 616cdd15027f..106bc39c7859 100644 --- a/persistence/sql/migratest/fixtures/identity/2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json +++ b/persistence/sql/migratest/fixtures/identity/2ae6a5a7-2983-49e7-a4d8-7740b37c88cb.json @@ -1,7 +1,7 @@ { "id": "2ae6a5a7-2983-49e7-a4d8-7740b37c88cb", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "d7b10@ory.sh" diff --git a/persistence/sql/migratest/fixtures/identity/359963ec-b09b-4ea0-aece-fb4dd95f304a.json b/persistence/sql/migratest/fixtures/identity/359963ec-b09b-4ea0-aece-fb4dd95f304a.json index ac8e011406f0..120e8ea08afe 100644 --- a/persistence/sql/migratest/fixtures/identity/359963ec-b09b-4ea0-aece-fb4dd95f304a.json +++ b/persistence/sql/migratest/fixtures/identity/359963ec-b09b-4ea0-aece-fb4dd95f304a.json @@ -1,7 +1,7 @@ { "id": "359963ec-b09b-4ea0-aece-fb4dd95f304a", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "d7b11@ory.sh" diff --git a/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json b/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json index 6d15a49d7fca..b6e9ed0bc41b 100644 --- a/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json +++ b/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json @@ -1,7 +1,7 @@ { "id": "5ff66179-c240-4703-b0d8-494592cefff5", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "bazbar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json b/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json index a47b50a5b18c..9baa12f444db 100644 --- a/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json +++ b/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json @@ -1,7 +1,7 @@ { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json b/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json index 22d408245a3a..bc860b29d0bc 100644 --- a/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json +++ b/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json @@ -1,7 +1,7 @@ { "id": "d7b9addb-ac15-4bc2-9fa5-562e0bf48755", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "d7b9@ory.sh" diff --git a/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json b/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json index bc51a073c71e..4ccac7db35f8 100644 --- a/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json +++ b/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json @@ -1,7 +1,7 @@ { "id": "ed253b2c-48ed-4c58-9b6f-1dc963c30a66", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "bazbar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f78b.json b/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f78b.json index 9766cccc1f04..6c62ae6a75e0 100644 --- a/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f78b.json +++ b/persistence/sql/migratest/fixtures/session/7458af86-c1d8-401c-978a-8da89133f78b.json @@ -18,7 +18,7 @@ "identity": { "id": "5ff66179-c240-4703-b0d8-494592cefff5", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "bazbar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json b/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json index aab301fa70cc..8f8069314676 100644 --- a/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json +++ b/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json @@ -14,7 +14,7 @@ "identity": { "id": "5ff66179-c240-4703-b0d8-494592cefff5", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "bazbar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/session/dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json b/persistence/sql/migratest/fixtures/session/dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json index ced9f1334cc4..95b62a5d8f68 100644 --- a/persistence/sql/migratest/fixtures/session/dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json +++ b/persistence/sql/migratest/fixtures/session/dcde5aaa-f789-4d3d-ae1f-76da8d57e67c.json @@ -14,7 +14,7 @@ "identity": { "id": "5ff66179-c240-4703-b0d8-494592cefff5", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "bazbar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json b/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json index 2ff9eface32b..ec1a51759fcd 100644 --- a/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json +++ b/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json @@ -14,7 +14,7 @@ "identity": { "id": "5ff66179-c240-4703-b0d8-494592cefff5", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "bazbar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json b/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json index bc1d05504754..7f2f9f2f213b 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json +++ b/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json @@ -12,7 +12,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json index 07c740c55dea..0e0f30c76887 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json +++ b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json b/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json index 4da4a77e0ab9..4a07c3a40eee 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json +++ b/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json b/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json index 21a60549296d..71c6c2d761ac 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json +++ b/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json @@ -12,7 +12,7 @@ "identity": { "id": "5ff66179-c240-4703-b0d8-494592cefff5", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "bazbar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json b/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json index 85187d50bdc5..ea1be0f89479 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json +++ b/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json b/persistence/sql/migratest/fixtures/settings_flow/8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json index 1928b0ac98b9..2e781c81dfb7 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json +++ b/persistence/sql/migratest/fixtures/settings_flow/8248bb5d-8ef7-45e3-8e07-9e2003dd5352.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json b/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json index d7da01c6a3c0..3d6d58ba6cc7 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json +++ b/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json b/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json index f432bae62c8e..02c81d878a85 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json +++ b/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json b/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json index d170900f6b8a..bd847b0de810 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json +++ b/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json b/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json index 58d0139f4af0..9273d7707333 100644 --- a/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json +++ b/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json @@ -13,7 +13,7 @@ "identity": { "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e", "schema_id": "default", - "schema_url": "https://www.ory.sh/schemas/default", + "schema_url": "https://www.ory.sh/schemas/ZGVmYXVsdA", "state": "active", "traits": { "email": "foobar@ory.sh" diff --git a/schema/handler.go b/schema/handler.go index 3639b2147804..525e7be622a6 100644 --- a/schema/handler.go +++ b/schema/handler.go @@ -91,14 +91,18 @@ func (h *Handler) getByID(w http.ResponseWriter, r *http.Request, ps httprouter. } id := ps.ByName("id") - if dec, err := base64.RawURLEncoding.DecodeString(ps.ByName("id")); err == nil { - id = string(dec) - } - s, err := ss.GetByID(id) if err != nil { - h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound.WithDebugf("%+v", err))) - return + // Maybe it is a base64 encoded ID? + if dec, err := base64.RawURLEncoding.DecodeString(id); err == nil { + id = string(dec) + } + + s, err = ss.GetByID(id) + if err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound.WithDebugf("%+v", err))) + return + } } src, err := ReadSchema(s) From dc9ceb50419d92012bc6c0569633287b92549aed Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 14:40:28 +0100 Subject: [PATCH 12/15] fix: properly check for not found --- selfservice/strategy/link/strategy_recovery.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 33ba3cd81887..6012dd2a61d6 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -159,7 +159,7 @@ func (s *Strategy) createRecoveryLink(w http.ResponseWriter, r *http.Request, _ } id, err := s.d.IdentityPool().GetIdentity(r.Context(), p.IdentityID) - if errors.Is(err, herodot.ErrNotFound) { + if errors.Is(err, sqlcon.ErrNoRows) { s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("The requested identity id does not exist.").WithWrap(err))) return } else if err != nil { From da677f64dc5de167b2491c94e537296d4e4569c1 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 14:40:40 +0100 Subject: [PATCH 13/15] test: remove obsolete test --- .../strategy/password/registration_test.go | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index 0584e41a370d..259052cc3707 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -389,37 +389,6 @@ func TestRegistration(t *testing.T) { }) }) - t.Run("case=should fail because schema did not specify an identifier", func(t *testing.T) { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/missing-identifier.schema.json") - - values := func(values url.Values) { - values.Set("method", "password") - values.Set("traits.username", "registration-identifier-6") - values.Set("password", x.NewUUID().String()) - values.Set("traits.foobar", "bar") - } - - t.Run("type=api", func(t *testing.T) { - body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS, values, - false, http.StatusBadRequest, - publicTS.URL+registration.RouteSubmitFlow) - assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find any login identifiers. Did you forget to set them?", "%s", body) - }) - - t.Run("type=spa", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookies(t) - body := testhelpers.SubmitRegistrationForm(t, false, browserClient, publicTS, values, - true, http.StatusBadRequest, - publicTS.URL+registration.RouteSubmitFlow) - assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find any login identifiers. Did you forget to set them?", "%s", body) - }) - - t.Run("type=browser", func(t *testing.T) { - body := expectValidationError(t, false, false, values) - assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find any login identifiers. Did you forget to set them?", "%s", body) - }) - }) - t.Run("case=should fail because schema does not exist", func(t *testing.T) { var check = func(t *testing.T, actual string) { assert.Equal(t, int64(http.StatusInternalServerError), gjson.Get(actual, "code").Int(), "%s", actual) From 4e87c3b95f30c35cbe00080f31f213da70836dc8 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 25 Feb 2022 15:22:59 +0100 Subject: [PATCH 14/15] test(e2e): resolve regressions --- .../cypress/integration/profiles/email/login/success.spec.ts | 4 ++-- .../integration/profiles/email/registration/success.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/cypress/integration/profiles/email/login/success.spec.ts b/test/e2e/cypress/integration/profiles/email/login/success.spec.ts index 12d0f24c060a..a6674f59bcdb 100644 --- a/test/e2e/cypress/integration/profiles/email/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/email/login/success.spec.ts @@ -42,7 +42,7 @@ describe('Basic email profile with succeeding login flows', () => { const { identity } = session expect(identity.id).to.not.be.empty expect(identity.schema_id).to.equal('default') - expect(identity.schema_url).to.equal(`${APP_URL}/schemas/default`) + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) expect(identity.traits.website).to.equal(website) expect(identity.traits.email).to.equal(email) }) @@ -60,7 +60,7 @@ describe('Basic email profile with succeeding login flows', () => { const { identity } = session expect(identity.id).to.not.be.empty expect(identity.schema_id).to.equal('default') - expect(identity.schema_url).to.equal(`${APP_URL}/schemas/default`) + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) expect(identity.traits.website).to.equal(website) expect(identity.traits.email).to.equal(email) }) diff --git a/test/e2e/cypress/integration/profiles/email/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/email/registration/success.spec.ts index d5a293806223..2730d75071dc 100644 --- a/test/e2e/cypress/integration/profiles/email/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/email/registration/success.spec.ts @@ -47,7 +47,7 @@ context('Registration success with email profile', () => { expect(identity.id).to.not.be.empty expect(identity.verifiable_addresses).to.have.length(1) expect(identity.schema_id).to.equal('default') - expect(identity.schema_url).to.equal(`${APP_URL}/schemas/default`) + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) expect(identity.traits.website).to.equal(website) expect(identity.traits.email).to.equal(email) expect(identity.traits.age).to.equal(age) @@ -73,7 +73,7 @@ context('Registration success with email profile', () => { expect(identity.id).to.not.be.empty expect(identity.verifiable_addresses).to.have.length(1) expect(identity.schema_id).to.equal('default') - expect(identity.schema_url).to.equal(`${APP_URL}/schemas/default`) + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) expect(identity.traits.website).to.equal(website) expect(identity.traits.email).to.equal(email) expect(identity.traits.age).to.be.undefined From 64ecfc21734d3f5dcf36d854ce352ae4e24d3374 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Sat, 26 Feb 2022 12:07:39 +0100 Subject: [PATCH 15/15] test: significantly reduce persister run time --- .gitignore | 4 +- go.mod | 2 +- go.sum | 5 +- persistence/sql/persister_credentials_test.go | 25 ----- persistence/sql/persister_identity_test.go | 59 ----------- persistence/sql/persister_test.go | 97 +++++++++++++++---- 6 files changed, 84 insertions(+), 108 deletions(-) delete mode 100644 persistence/sql/persister_credentials_test.go delete mode 100644 persistence/sql/persister_identity_test.go diff --git a/.gitignore b/.gitignore index 0d3b0ffaeb65..0297bbc51271 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ test/e2e/.bin pkged.go coverage.* schema.sql - +heap_profiler/ +goroutine_dump/ +inflight_trace_dump/ diff --git a/go.mod b/go.mod index b2e99b81671e..e8b9acf82128 100644 --- a/go.mod +++ b/go.mod @@ -76,7 +76,7 @@ require ( github.com/ory/kratos-client-go v0.6.3-alpha.1 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.348 + github.com/ory/x v0.0.351 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 diff --git a/go.sum b/go.sum index db4e460ee60e..3317104214a6 100644 --- a/go.sum +++ b/go.sum @@ -555,6 +555,7 @@ github.com/envoyproxy/protoc-gen-validate v0.6.1/go.mod h1:txg5va2Qkip90uYoSKH+n github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= +github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= @@ -1804,8 +1805,8 @@ github.com/ory/x v0.0.205/go.mod h1:A1s4iwmFIppRXZLF3J9GGWeY/HpREVm0Dk5z/787iek= github.com/ory/x v0.0.250/go.mod h1:jUJaVptu+geeqlb9SyQCogTKj5ztSDIF6APkhbKtwLc= github.com/ory/x v0.0.272/go.mod h1:1TTPgJGQutrhI2OnwdrTIHE9ITSf4MpzXFzA/ncTGRc= github.com/ory/x v0.0.288/go.mod h1:APpShLyJcVzKw1kTgrHI+j/L9YM+8BRjHlcYObc7C1U= -github.com/ory/x v0.0.348 h1:Z2wbEvSpTindtjKTTrd3grIlWbBtvW2udYG5ZjTZHTo= -github.com/ory/x v0.0.348/go.mod h1:Ddbu3ecSaNDgxdntdD1gDu3ALG5fWR5AwUB1ILeBUNE= +github.com/ory/x v0.0.351 h1:RkiK5MH7rCm461SmHvGJopHWGToTkGLaC8VOrTis6cM= +github.com/ory/x v0.0.351/go.mod h1:zuNjBKtyxFpKnDG6q/1QP0pqQv840P/Dw5JbdY7lNTU= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= diff --git a/persistence/sql/persister_credentials_test.go b/persistence/sql/persister_credentials_test.go deleted file mode 100644 index 3e7380607024..000000000000 --- a/persistence/sql/persister_credentials_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package sql_test - -import ( - "context" - "fmt" - "testing" - - "github.com/ory/kratos/persistence/sql" - - "github.com/stretchr/testify/require" - - "github.com/ory/kratos/identity" -) - -func TestCredentialTypes(t *testing.T) { - ps := createCleanDatabases(t) - - for name, p := range ps { - t.Run(fmt.Sprintf("db=%s", name), func(t *testing.T) { - for _, ct := range []identity.CredentialsType{identity.CredentialsTypeOIDC, identity.CredentialsTypePassword} { - require.NoError(t, p.Persister().(*sql.Persister).Connection(context.Background()).Where("name = ?", ct).First(&identity.CredentialsTypeTable{})) - } - }) - } -} diff --git a/persistence/sql/persister_identity_test.go b/persistence/sql/persister_identity_test.go deleted file mode 100644 index 2e4fb93d16b6..000000000000 --- a/persistence/sql/persister_identity_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package sql_test - -import ( - "context" - "fmt" - "sync" - "testing" - - "github.com/ory/kratos/internal/testhelpers" - - "github.com/stretchr/testify/require" - - "github.com/ory/kratos/driver/config" - "github.com/ory/kratos/identity" - "github.com/ory/kratos/schema" - "github.com/ory/x/sqlxx" - "github.com/ory/x/urlx" -) - -// note: it is important that this test runs on clean databases, as the racy behaviour only happens there -func TestPersister_CreateIdentityRacy(t *testing.T) { - defaultSchema := schema.Schema{ - ID: config.DefaultIdentityTraitsSchemaID, - URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"), - RawURL: "file://./stub/identity.schema.json", - } - - ctx := context.Background() - - for name, p := range createCleanDatabases(t) { - t.Run(fmt.Sprintf("db=%s", name), func(t *testing.T) { - var wg sync.WaitGroup - testhelpers.SetDefaultIdentitySchema(p.Config(context.Background()), defaultSchema.RawURL) - _, ps := testhelpers.NewNetwork(t, ctx, p.Persister()) - - for i := 0; i < 10; i++ { - wg.Add(1) - // capture i - ii := i - go func() { - defer wg.Done() - - id := identity.NewIdentity("") - id.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ - Type: identity.CredentialsTypePassword, - Identifiers: []string{fmt.Sprintf("racy identity %d", ii)}, - Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), - }) - id.Traits = identity.Traits("{}") - - require.NoError(t, ps.CreateIdentity(context.Background(), id)) - }() - } - - wg.Wait() - }) - - } -} diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go index 890abc8bf3b6..26db5eec81db 100644 --- a/persistence/sql/persister_test.go +++ b/persistence/sql/persister_test.go @@ -1,9 +1,12 @@ package sql_test import ( - "bytes" "context" "fmt" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/schema" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" "os" "path/filepath" "sync" @@ -94,9 +97,10 @@ func createCleanDatabases(t *testing.T) map[string]*driver.RegistryDefault { var l sync.Mutex if !testing.Short() { funcs := map[string]func(t testing.TB) string{ - "postgres": dockertest.RunTestPostgreSQL, - "mysql": dockertest.RunTestMySQL, - "cockroach": dockertest.RunTestCockroachDB} + "postgres": dockertest.RunTestPostgreSQL, + // "mysql": dockertest.RunTestMySQL, + "cockroach": dockertest.NewLocalTestCRDBServer, + } var wg sync.WaitGroup wg.Add(len(funcs)) @@ -117,30 +121,40 @@ func createCleanDatabases(t *testing.T) map[string]*driver.RegistryDefault { t.Logf("sqlite: %s", sqlite) ps := make(map[string]*driver.RegistryDefault, len(conns)) + var wg sync.WaitGroup + wg.Add(len(conns)) for name, dsn := range conns { - _, reg := internal.NewRegistryDefaultWithDSN(t, dsn) - p := reg.Persister().(*sql.Persister) + go func(name, dsn string) { + defer wg.Done() + t.Logf("Connecting to %s", name) + _, reg := internal.NewRegistryDefaultWithDSN(t, dsn) + p := reg.Persister().(*sql.Persister) - _ = os.Remove("migrations/schema.sql") - xsql.CleanSQL(t, p.Connection(context.Background())) - t.Cleanup(func() { - xsql.CleanSQL(t, p.Connection(context.Background())) + t.Logf("Cleaning up %s", name) _ = os.Remove("migrations/schema.sql") - }) + xsql.CleanSQL(t, p.Connection(context.Background())) + t.Cleanup(func() { + xsql.CleanSQL(t, p.Connection(context.Background())) + _ = os.Remove("migrations/schema.sql") + }) - pop.SetLogger(pl(t)) - require.NoError(t, p.MigrateUp(context.Background())) - status, err := p.MigrationStatus(context.Background()) - require.NoError(t, err) - require.False(t, status.HasPending()) + t.Logf("Applying %s migrations", name) + pop.SetLogger(pl(t)) + require.NoError(t, p.MigrateUp(context.Background())) + t.Logf("%s migrations applied", name) + status, err := p.MigrationStatus(context.Background()) + require.NoError(t, err) + require.False(t, status.HasPending()) - var b bytes.Buffer - require.NoError(t, status.Write(&b)) - t.Logf("%s", b.String()) + l.Lock() + ps[name] = reg + l.Unlock() - ps[name] = reg + t.Logf("Database %s initialized successfully", name) + }(name, dsn) } + wg.Wait() return ps } @@ -150,10 +164,53 @@ func TestPersister(t *testing.T) { for name, reg := range conns { t.Run(fmt.Sprintf("database=%s", name), func(t *testing.T) { + t.Parallel() + _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) conf := reg.Config(context.Background()) t.Logf("DSN: %s", conf.DSN()) + + // This test must remain the first test in the test suite! + t.Run("racy identity creation", func(t *testing.T) { + defaultSchema := schema.Schema{ + ID: config.DefaultIdentityTraitsSchemaID, + URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"), + RawURL: "file://./stub/identity.schema.json", + } + + var wg sync.WaitGroup + testhelpers.SetDefaultIdentitySchema(reg.Config(context.Background()), defaultSchema.RawURL) + _, ps := testhelpers.NewNetwork(t, ctx, reg.Persister()) + + for i := 0; i < 10; i++ { + wg.Add(1) + // capture i + ii := i + go func() { + defer wg.Done() + + id := ri.NewIdentity("") + id.SetCredentials(ri.CredentialsTypePassword, ri.Credentials{ + Type: ri.CredentialsTypePassword, + Identifiers: []string{fmt.Sprintf("racy identity %d", ii)}, + Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), + }) + id.Traits = ri.Traits("{}") + + require.NoError(t, ps.CreateIdentity(context.Background(), id)) + }() + } + + wg.Wait() + }) + + t.Run("case=credentials types", func(t *testing.T) { + for _, ct := range []ri.CredentialsType{ri.CredentialsTypeOIDC, ri.CredentialsTypePassword} { + require.NoError(t, p.(*sql.Persister).Connection(context.Background()).Where("name = ?", ct).First(&ri.CredentialsTypeTable{})) + } + }) + t.Run("contract=identity.TestPool", func(t *testing.T) { pop.SetLogger(pl(t)) identity.TestPool(ctx, conf, p)(t)