From 77afc6f8ea868eaba7853adfcb9ed159b44ecbc8 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 1 Mar 2022 09:34:16 +0100 Subject: [PATCH] feat: add credential migrator pattern --- ...tUpgradeCredentials-empty_credentials.json | 9 +++ ...radeCredentials-type=webauthn-from=v0.json | 38 +++++++++++ credentialmigrate/migrate.go | 54 +++++++++++++++ credentialmigrate/migrate_test.go | 68 +++++++++++++++++++ credentialmigrate/stub/webauthn/v0.json | 16 +++++ credentialmigrate/stub/webauthn/v1.json | 18 +++++ 6 files changed, 203 insertions(+) create mode 100644 credentialmigrate/.snapshots/TestUpgradeCredentials-empty_credentials.json create mode 100644 credentialmigrate/.snapshots/TestUpgradeCredentials-type=webauthn-from=v0.json create mode 100644 credentialmigrate/migrate.go create mode 100644 credentialmigrate/migrate_test.go create mode 100644 credentialmigrate/stub/webauthn/v0.json create mode 100644 credentialmigrate/stub/webauthn/v1.json diff --git a/credentialmigrate/.snapshots/TestUpgradeCredentials-empty_credentials.json b/credentialmigrate/.snapshots/TestUpgradeCredentials-empty_credentials.json new file mode 100644 index 000000000000..21cd2aa56a03 --- /dev/null +++ b/credentialmigrate/.snapshots/TestUpgradeCredentials-empty_credentials.json @@ -0,0 +1,9 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/credentialmigrate/.snapshots/TestUpgradeCredentials-type=webauthn-from=v0.json b/credentialmigrate/.snapshots/TestUpgradeCredentials-type=webauthn-from=v0.json new file mode 100644 index 000000000000..8c633bb7ebd6 --- /dev/null +++ b/credentialmigrate/.snapshots/TestUpgradeCredentials-type=webauthn-from=v0.json @@ -0,0 +1,38 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "webauthn": { + "type": "webauthn", + "identifiers": [ + "4d64fa08-20fc-450d-bebd-ebd7c7b6e249" + ], + "config": { + "credentials": [ + { + "id": "HQ4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z", + "is_passwordless": false, + "user_handle": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/credentialmigrate/migrate.go b/credentialmigrate/migrate.go new file mode 100644 index 000000000000..0e29a40b5697 --- /dev/null +++ b/credentialmigrate/migrate.go @@ -0,0 +1,54 @@ +package credentialmigrate + +import ( + "encoding/json" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/strategy/webauthn" + "github.com/pkg/errors" +) + +// UpgradeWebAuthnCredential migrates a webauthn credential from an older version to a newer version. +func UpgradeWebAuthnCredential(i *identity.Identity, ic *identity.Credentials, c *webauthn.CredentialsConfig) { + if ic.Version == 0 { + for k := range c.Credentials { + c.Credentials[k].UserHandle = i.ID.String() + + // We do not set c.IsPasswordless as it defaults to false anyways, which is the correct migration . + } + + ic.Version = 1 + } +} + +func UpgradeWebAuthnCredentials(i *identity.Identity, c *identity.Credentials) error { + if c.Type != identity.CredentialsTypeWebAuthn { + return nil + } + + var cred webauthn.CredentialsConfig + if err := json.Unmarshal(c.Config, &cred); err != nil { + return errors.WithStack(err) + } + + UpgradeWebAuthnCredential(i, c, &cred) + + updatedConf, err := json.Marshal(&cred) + if err != nil { + return errors.WithStack(err) + } + + c.Config = updatedConf + return nil +} + +// UpgradeCredentials migrates a set of older WebAuthn credentials to newer ones. +func UpgradeCredentials(i *identity.Identity) error { + for k := range i.Credentials { + c := i.Credentials[k] + if err := UpgradeWebAuthnCredentials(i, &c); err != nil { + return errors.WithStack(err) + } + i.Credentials[k] = c + } + return nil +} diff --git a/credentialmigrate/migrate_test.go b/credentialmigrate/migrate_test.go new file mode 100644 index 000000000000..ed2813eb2de2 --- /dev/null +++ b/credentialmigrate/migrate_test.go @@ -0,0 +1,68 @@ +package credentialmigrate + +import ( + _ "embed" + "github.com/gofrs/uuid" + "github.com/ory/kratos/identity" + "github.com/ory/x/snapshotx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +//go:embed stub/webauthn/v0.json +var webAuthnV0 []byte + +//go:embed stub/webauthn/v1.json +var webAuthnV1 []byte + +func TestUpgradeCredentials(t *testing.T) { + t.Run("empty credentials", func(t *testing.T) { + i := &identity.Identity{} + + err := UpgradeCredentials(i) + require.NoError(t, err) + wc := identity.WithCredentialsInJSON(*i) + snapshotx.SnapshotTExcept(t, &wc, nil) + }) + + identityID := uuid.FromStringOrNil("4d64fa08-20fc-450d-bebd-ebd7c7b6e249") + t.Run("type=webauthn", func(t *testing.T) { + t.Run("from=v0", func(t *testing.T) { + i := &identity.Identity{ + ID: identityID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeWebAuthn: { + Identifiers: []string{"4d64fa08-20fc-450d-bebd-ebd7c7b6e249"}, + Type: identity.CredentialsTypeWebAuthn, + Version: 0, + Config: webAuthnV0, + }}, + } + + require.NoError(t, UpgradeCredentials(i)) + wc := identity.WithCredentialsInJSON(*i) + snapshotx.SnapshotTExcept(t, &wc, nil) + + assert.Equal(t, 1, i.Credentials[identity.CredentialsTypeWebAuthn].Version) + }) + + t.Run("from=v1", func(t *testing.T) { + i := &identity.Identity{ + ID: identityID, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeWebAuthn: { + Type: identity.CredentialsTypeWebAuthn, + Version: 1, + Config: webAuthnV1, + }}, + } + + require.NoError(t, UpgradeCredentials(i)) + wc := identity.WithCredentialsInJSON(*i) + snapshotx.SnapshotTExcept(t, &wc, nil) + + assert.Equal(t, 1, i.Credentials[identity.CredentialsTypeWebAuthn].Version) + }) + }) +} diff --git a/credentialmigrate/stub/webauthn/v0.json b/credentialmigrate/stub/webauthn/v0.json new file mode 100644 index 000000000000..5845232120b5 --- /dev/null +++ b/credentialmigrate/stub/webauthn/v0.json @@ -0,0 +1,16 @@ +{ + "credentials": [ + { + "id": "HQ4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z" + } + ] +} diff --git a/credentialmigrate/stub/webauthn/v1.json b/credentialmigrate/stub/webauthn/v1.json new file mode 100644 index 000000000000..74ca836a3de3 --- /dev/null +++ b/credentialmigrate/stub/webauthn/v1.json @@ -0,0 +1,18 @@ +{ + "credentials": [ + { + "id": "HQ4LaIJ9NiqS1r0CQpWY+K0gMvhOq4yk5BHuO/YlitcurSpBK7weDXOvBcuN4lvn6DAmjGfmj/J/6bpOmtdT8Q==", + "public_key": "pQECAyYgASFYILAYFLoH1T8bQMSbPrNBCMMS5U7OFWRwv2U+GkAoiBADIlggBv+8ni7XVZYBB8ufMbP/d9fDxbmOkVVHOgcJifnoOR4=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 4, + "clone_warning": false + }, + "display_name": "asdf", + "added_at": "2022-02-28T16:40:39Z", + "is_passwordless": true, + "user_handle": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249" + } + ] +}