-
-
Notifications
You must be signed in to change notification settings - Fork 963
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
10 changed files
with
300 additions
and
16 deletions.
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
...ty/.snapshots/TestHandler-case=should_be_able_to_import_users-with_argon2id_password.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"credentials": { | ||
"password": { | ||
"type": "password", | ||
"identifiers": [ | ||
"[email protected]" | ||
], | ||
"config": { | ||
} | ||
} | ||
}, | ||
"schema_id": "default", | ||
"state": "active", | ||
"traits": { | ||
"email": "[email protected]" | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...ity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_bcrypt2_password.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"credentials": { | ||
"password": { | ||
"type": "password", | ||
"identifiers": [ | ||
"[email protected]" | ||
], | ||
"config": { | ||
} | ||
} | ||
}, | ||
"schema_id": "default", | ||
"state": "active", | ||
"traits": { | ||
"email": "[email protected]" | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
...ler-case=should_be_able_to_import_users-with_cleartext_password_and_oidc_credentials.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": [ | ||
"[email protected]" | ||
], | ||
"config": { | ||
} | ||
} | ||
}, | ||
"schema_id": "default", | ||
"state": "active", | ||
"traits": { | ||
"email": "[email protected]" | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...tity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_pkbdf2_password.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"credentials": { | ||
"password": { | ||
"type": "password", | ||
"identifiers": [ | ||
"[email protected]" | ||
], | ||
"config": { | ||
} | ||
} | ||
}, | ||
"schema_id": "default", | ||
"state": "active", | ||
"traits": { | ||
"email": "[email protected]" | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
...y/.snapshots/TestHandler-case=should_be_able_to_import_users-without_any_credentials.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"credentials": { | ||
"password": { | ||
"type": "password", | ||
"identifiers": [ | ||
"[email protected]" | ||
], | ||
"config": {} | ||
} | ||
}, | ||
"schema_id": "default", | ||
"state": "active", | ||
"traits": { | ||
"email": "[email protected]" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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": "[email protected]"}`)}) | ||
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": "[email protected]"}`), | ||
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": "[email protected]"}`), | ||
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": "[email protected]"}`), | ||
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": "[email protected]"}`), | ||
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) { | ||
|
Oops, something went wrong.