Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
730 lines
26 KiB
Go
730 lines
26 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package identity
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
|
|
"github.com/ory/kratos/cipher"
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/x"
|
|
"github.com/ory/x/snapshotx"
|
|
"github.com/ory/x/sqlxx"
|
|
)
|
|
|
|
func TestNewIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
assert.Equal(t, uuid.Nil, i.ID)
|
|
assert.NotEmpty(t, i.Traits)
|
|
assert.NotNil(t, i.Credentials)
|
|
assert.True(t, i.IsActive())
|
|
}
|
|
|
|
func TestIdentityCredentialsOr(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Credentials = nil
|
|
|
|
expected := &Credentials{ID: x.NewUUID(), Type: CredentialsTypePassword}
|
|
assert.Equal(t, expected, i.GetCredentialsOr(CredentialsTypePassword, expected))
|
|
|
|
expected = &Credentials{ID: x.NewUUID(), Type: CredentialsTypeWebAuthn}
|
|
i.SetCredentials(CredentialsTypeWebAuthn, *expected)
|
|
|
|
assert.Equal(t, expected, i.GetCredentialsOr(CredentialsTypeWebAuthn, nil))
|
|
}
|
|
|
|
func TestIdentityCredentialsOrCreate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Credentials = nil
|
|
|
|
expected := &Credentials{Config: []byte("true"), IdentityID: i.ID, Type: CredentialsTypePassword}
|
|
i.UpsertCredentialsConfig(CredentialsTypePassword, []byte("true"), 0)
|
|
actual, ok := i.GetCredentials(CredentialsTypePassword)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
|
|
func TestIdentityCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Credentials = nil
|
|
|
|
// Shouldn't error if map is nil
|
|
i.DeleteCredentialsType(CredentialsTypeTOTP)
|
|
|
|
expectedTOTP := Credentials{ID: x.NewUUID(), Type: CredentialsTypeTOTP}
|
|
i.SetCredentials(CredentialsTypeTOTP, expectedTOTP)
|
|
actual, found := i.GetCredentials(CredentialsTypeTOTP)
|
|
assert.True(t, found, "should set and find the credential if map was nil")
|
|
assert.Equal(t, &expectedTOTP, actual)
|
|
|
|
expectedPassword := Credentials{ID: x.NewUUID(), Type: CredentialsTypePassword}
|
|
i.SetCredentials(CredentialsTypePassword, expectedPassword)
|
|
actual, found = i.GetCredentials(CredentialsTypePassword)
|
|
assert.True(t, found, "should set and find the credential if map was not nil")
|
|
assert.Equal(t, &expectedPassword, actual)
|
|
|
|
expectedOIDC := Credentials{ID: x.NewUUID()}
|
|
i.SetCredentials(CredentialsTypeOIDC, expectedOIDC)
|
|
actual, found = i.GetCredentials(CredentialsTypeOIDC)
|
|
assert.True(t, found)
|
|
assert.Equal(t, expectedOIDC.ID, actual.ID)
|
|
assert.Equal(t, CredentialsTypeOIDC, actual.Type, "should set the type if we forgot to set it in the credentials struct")
|
|
|
|
i.DeleteCredentialsType(CredentialsTypePassword)
|
|
_, found = i.GetCredentials(CredentialsTypePassword)
|
|
assert.False(t, found, "should delete a credential properly")
|
|
|
|
actual, found = i.GetCredentials(CredentialsTypeTOTP)
|
|
assert.True(t, found, "but not alter other credentials")
|
|
assert.Equal(t, &expectedTOTP, actual)
|
|
}
|
|
|
|
func TestMarshalExcludesCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Credentials = map[CredentialsType]Credentials{
|
|
CredentialsTypePassword: {
|
|
ID: uuid.UUID{},
|
|
},
|
|
}
|
|
|
|
rawJSON, err := json.Marshal(i)
|
|
require.NoError(t, err)
|
|
|
|
creds := gjson.GetBytes(rawJSON, "credentials")
|
|
assert.Falsef(t, creds.Exists(), "Credentials should not be rendered to JSON, but got: %q", creds.Raw)
|
|
|
|
// To ensure the original identity is not changed / Unmarshal has no side effects:
|
|
require.NotEmpty(t, i.Credentials)
|
|
}
|
|
|
|
func TestMarshalExcludesCredentialsByReference(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Credentials = map[CredentialsType]Credentials{
|
|
CredentialsTypePassword: {
|
|
ID: uuid.UUID{},
|
|
},
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
require.Nil(t, json.NewEncoder(&b).Encode(&i))
|
|
|
|
assert.False(t, gjson.Get(b.String(), "credentials").Exists(), "Credentials should not be rendered to json")
|
|
|
|
// To ensure the original identity is not changed / Unmarshal has no side effects:
|
|
require.NotEmpty(t, i.Credentials)
|
|
}
|
|
|
|
func TestMarshalIgnoresAdminMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.MetadataAdmin = []byte(`{"admin":"bar"}`)
|
|
i.MetadataPublic = []byte(`{"public":"bar"}`)
|
|
|
|
var b bytes.Buffer
|
|
require.Nil(t, json.NewEncoder(&b).Encode(&i))
|
|
|
|
assert.False(t, gjson.Get(b.String(), "metadata_admin.admin").Exists(), "Admin metadata should not be rendered to json but got: %s", b.String())
|
|
assert.Equal(t, "bar", gjson.Get(b.String(), "metadata_public.public").String(), "Public metadata should be rendered to json")
|
|
|
|
// To ensure the original identity is not changed / Unmarshal has no side effects:
|
|
require.NotEmpty(t, i.MetadataAdmin)
|
|
require.NotEmpty(t, i.MetadataPublic)
|
|
}
|
|
|
|
func TestUnMarshallIgnoresCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
jsonText := `{"id":"3234ad11-49c6-49e2-bfac-537f3e06cd85","schema_id":"default","schema_url":"","traits":{}, "credentials" : {"password":{"type":"","identifiers":null,"config":null,"updatedAt":"0001-01-01T00:00:00Z"}}}`
|
|
var i Identity
|
|
err := json.Unmarshal([]byte(jsonText), &i)
|
|
assert.Nil(t, err)
|
|
|
|
assert.Nil(t, i.Credentials)
|
|
assert.Equal(t, "3234ad11-49c6-49e2-bfac-537f3e06cd85", i.ID.String())
|
|
}
|
|
|
|
func TestUnMarshallIgnoresAdminMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
jsonText := `{"id":"3234ad11-49c6-49e2-bfac-537f3e06cd85","schema_id":"default","schema_url":"","traits":{}, "admin_metadata" : {"foo":"bar"}}`
|
|
var i Identity
|
|
err := json.Unmarshal([]byte(jsonText), &i)
|
|
assert.Nil(t, err)
|
|
|
|
assert.Nil(t, i.MetadataAdmin)
|
|
}
|
|
|
|
func TestMarshalIdentityWithCredentialsWhenCredentialsNil(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Credentials = nil
|
|
|
|
var b bytes.Buffer
|
|
require.Nil(t, json.NewEncoder(&b).Encode(WithCredentialsNoConfigAndAdminMetadataInJSON(*i)))
|
|
|
|
assert.False(t, gjson.Get(b.String(), "credentials").Exists())
|
|
}
|
|
|
|
func TestMarshalIdentityWithAdminMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.MetadataAdmin = []byte(`{"some":"metadata"}`)
|
|
|
|
var b bytes.Buffer
|
|
require.Nil(t, json.NewEncoder(&b).Encode(WithAdminMetadataInJSON(*i)))
|
|
assert.Equal(t, "metadata", gjson.GetBytes(i.MetadataAdmin, "some").String(), "Original metadata_admin should not be touched by marshalling")
|
|
}
|
|
|
|
func TestMarshalIdentityWithCredentialsNoConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
credentials := map[CredentialsType]Credentials{
|
|
CredentialsTypePassword: {
|
|
Type: CredentialsTypePassword,
|
|
Config: sqlxx.JSONRawMessage(`{"some":"secret"}`),
|
|
},
|
|
}
|
|
i.Credentials = credentials
|
|
i.MetadataAdmin = []byte(`{"some":"metadata"}`)
|
|
|
|
rawJSON, err := json.Marshal((*WithCredentialsNoConfigAndAdminMetadataInJSON)(i))
|
|
require.NoError(t, err)
|
|
|
|
credentialsInJSON := gjson.GetBytes(rawJSON, "credentials")
|
|
assert.Truef(t, credentialsInJSON.Exists(), "Credentials should be rendered to JSON, but got: %q", credentialsInJSON.Raw)
|
|
|
|
assert.JSONEq(t, `{"password":{"type":"password","identifiers":null,"updated_at":"0001-01-01T00:00:00Z","created_at":"0001-01-01T00:00:00Z","version":0}}`, credentialsInJSON.Raw)
|
|
assert.Equal(t, credentials, i.Credentials, "Original credentials should not be touched by marshalling")
|
|
assert.Equal(t, "metadata", gjson.GetBytes(i.MetadataAdmin, "some").String(), "Original metadata_admin should not be touched by marshalling")
|
|
}
|
|
|
|
func TestMarshalIdentityWithAll(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
credentials := map[CredentialsType]Credentials{
|
|
CredentialsTypePassword: {
|
|
Type: CredentialsTypePassword,
|
|
Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"),
|
|
},
|
|
}
|
|
i.Credentials = credentials
|
|
i.MetadataAdmin = []byte(`{"some":"metadata"}`)
|
|
|
|
var b bytes.Buffer
|
|
require.Nil(t, json.NewEncoder(&b).Encode(WithCredentialsAndAdminMetadataInJSON(*i)))
|
|
|
|
credentialsInJSON := gjson.Get(b.String(), "credentials")
|
|
assert.True(t, credentialsInJSON.Exists())
|
|
|
|
snapshotx.SnapshotT(t, json.RawMessage(credentialsInJSON.Raw))
|
|
assert.Equal(t, credentials, i.Credentials, "Original credentials should not be touched by marshalling")
|
|
assert.Equal(t, "metadata", gjson.GetBytes(i.MetadataAdmin, "some").String(), "Original credentials should not be touched by marshalling")
|
|
}
|
|
|
|
func TestValidateNID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nid := x.NewUUID()
|
|
for k, tc := range []struct {
|
|
i *Identity
|
|
expect *Identity
|
|
expectedErr bool
|
|
}{
|
|
{i: &Identity{}, expectedErr: true},
|
|
{i: &Identity{NID: nid}},
|
|
{
|
|
i: &Identity{NID: nid, RecoveryAddresses: []RecoveryAddress{{NID: x.NewUUID()}}},
|
|
expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{}},
|
|
},
|
|
{
|
|
i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: x.NewUUID()}}},
|
|
expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{}},
|
|
},
|
|
{
|
|
i: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: x.NewUUID()}}},
|
|
expect: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{}, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{}},
|
|
},
|
|
{
|
|
i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: x.NewUUID()}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}},
|
|
expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}},
|
|
},
|
|
{
|
|
i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: x.NewUUID()}}},
|
|
expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{}},
|
|
},
|
|
{
|
|
i: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}},
|
|
expect: &Identity{NID: nid, VerifiableAddresses: []VerifiableAddress{{NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}},
|
|
},
|
|
{
|
|
i: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: x.NewUUID()}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}},
|
|
expect: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}},
|
|
},
|
|
{
|
|
i: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}},
|
|
expect: &Identity{NID: nid, Credentials: map[CredentialsType]Credentials{CredentialsTypePassword: {NID: nid}}, RecoveryAddresses: []RecoveryAddress{{NID: nid}}, VerifiableAddresses: []VerifiableAddress{{NID: nid}}},
|
|
},
|
|
} {
|
|
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
|
err := tc.i.Validate()
|
|
if tc.expectedErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
if tc.expect != nil {
|
|
assert.EqualValues(t, tc.expect, tc.i)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRecoveryAddresses tests the CollectRecoveryAddresses are collected from all identities.
|
|
func TestRecoveryAddresses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var addresses []RecoveryAddress
|
|
|
|
for i := range 10 {
|
|
addresses = append(addresses, RecoveryAddress{
|
|
Value: fmt.Sprintf("address-%d", i),
|
|
})
|
|
}
|
|
|
|
id1 := &Identity{RecoveryAddresses: addresses[:5]}
|
|
id2 := &Identity{}
|
|
id3 := &Identity{RecoveryAddresses: addresses[5:]}
|
|
|
|
assert.Equal(t, addresses, CollectRecoveryAddresses([]*Identity{id1, id2, id3}))
|
|
}
|
|
|
|
// TestVerifiableAddresses tests the VerfifableAddresses are collected from all identities.
|
|
func TestVerifiableAddresses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var addresses []VerifiableAddress
|
|
|
|
for i := 0; i < 10; i++ {
|
|
addresses = append(addresses, VerifiableAddress{
|
|
Value: fmt.Sprintf("address-%d", i),
|
|
})
|
|
}
|
|
|
|
id1 := &Identity{VerifiableAddresses: addresses[:5]}
|
|
id2 := &Identity{}
|
|
id3 := &Identity{VerifiableAddresses: addresses[5:]}
|
|
|
|
assert.Equal(t, addresses, CollectVerifiableAddresses([]*Identity{id1, id2, id3}))
|
|
}
|
|
|
|
type cipherProvider struct{}
|
|
|
|
func (c *cipherProvider) Cipher(context.Context) cipher.Cipher {
|
|
return cipher.NewNoop()
|
|
}
|
|
|
|
func TestWithDeclassifiedCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
credentials := map[CredentialsType]Credentials{
|
|
CredentialsTypePassword: {
|
|
Identifiers: []string{"zab", "bar"},
|
|
Type: CredentialsTypePassword,
|
|
Config: sqlxx.JSONRawMessage(`{"some": "secret"}`),
|
|
},
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"bar", "baz"},
|
|
// hint:
|
|
// echo '666f6f' | xxd -r -p
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"subject":"bar","provider":"oidc1","initial_id_token":"666f6f"}]}`),
|
|
},
|
|
CredentialsTypeSAML: {
|
|
Type: CredentialsTypeSAML,
|
|
Identifiers: []string{"qux", "quz"},
|
|
// hint:
|
|
// echo 'this should not appear in output' | xxd -ps -c 0
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"subject":"qux","provider":"saml1","initial_id_token":"746869732073686f756c64206e6f742061707065617220696e206f75747075740a"}]}`),
|
|
},
|
|
CredentialsTypeWebAuthn: {
|
|
Type: CredentialsTypeWebAuthn,
|
|
Identifiers: []string{"foo", "bar"},
|
|
Config: sqlxx.JSONRawMessage(`{"some": "secret"}`),
|
|
},
|
|
}
|
|
i.Credentials = credentials
|
|
|
|
t.Run("case=no-include", func(t *testing.T) {
|
|
actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, nil)
|
|
require.NoError(t, err)
|
|
|
|
for ct, actual := range actualIdentity.Credentials {
|
|
t.Run("credential="+string(ct), func(t *testing.T) {
|
|
snapshotx.SnapshotT(t, actual)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=include-webauthn", func(t *testing.T) {
|
|
actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeWebAuthn})
|
|
require.NoError(t, err)
|
|
|
|
for ct, actual := range actualIdentity.Credentials {
|
|
t.Run("credential="+string(ct), func(t *testing.T) {
|
|
snapshotx.SnapshotT(t, actual)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=include-multi", func(t *testing.T) {
|
|
actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeWebAuthn, CredentialsTypePassword})
|
|
require.NoError(t, err)
|
|
|
|
for ct, actual := range actualIdentity.Credentials {
|
|
t.Run("credential="+string(ct), func(t *testing.T) {
|
|
snapshotx.SnapshotT(t, actual)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=oidc", func(t *testing.T) {
|
|
actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeOIDC})
|
|
require.NoError(t, err)
|
|
|
|
for ct, actual := range actualIdentity.Credentials {
|
|
t.Run("credential="+string(ct), func(t *testing.T) {
|
|
snapshotx.SnapshotT(t, actual)
|
|
})
|
|
}
|
|
})
|
|
t.Run("case=saml", func(t *testing.T) {
|
|
actualIdentity, err := i.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeSAML})
|
|
require.NoError(t, err)
|
|
|
|
for ct, actual := range actualIdentity.Credentials {
|
|
t.Run("credential="+string(ct), func(t *testing.T) {
|
|
snapshotx.SnapshotT(t, actual)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=deviceauthn", func(t *testing.T) {
|
|
di := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
di.Credentials = map[CredentialsType]Credentials{
|
|
CredentialsTypeDeviceAuthn: {
|
|
Type: CredentialsTypeDeviceAuthn,
|
|
Identifiers: []string{di.ID.String()},
|
|
Config: sqlxx.JSONRawMessage(`{"credentials":[` +
|
|
`{"client_key_id":"key-with-pin","device_name":"Pixel","state":"confirmed","pin":{"pin_secret":"deadbeefciphertext","failed_attempts":1}},` +
|
|
`{"client_key_id":"key-without-pin","device_name":"iPhone","state":"confirmed"}` +
|
|
`]}`),
|
|
},
|
|
}
|
|
|
|
actualIdentity, err := di.WithDeclassifiedCredentials(t.Context(), &cipherProvider{}, []CredentialsType{CredentialsTypeDeviceAuthn})
|
|
require.NoError(t, err)
|
|
|
|
actual, ok := actualIdentity.Credentials[CredentialsTypeDeviceAuthn]
|
|
require.True(t, ok)
|
|
|
|
// The PIN secret (and the whole pin object) must never reach the admin API.
|
|
assert.NotContains(t, string(actual.Config), "pin_secret")
|
|
assert.NotContains(t, string(actual.Config), "deadbeefciphertext")
|
|
assert.False(t, gjson.GetBytes(actual.Config, "credentials.0.pin").Exists(), "pin object must be stripped")
|
|
|
|
// Non-secret fields must be preserved.
|
|
assert.Equal(t, "key-with-pin", gjson.GetBytes(actual.Config, "credentials.0.client_key_id").String())
|
|
assert.Equal(t, "Pixel", gjson.GetBytes(actual.Config, "credentials.0.device_name").String())
|
|
assert.Equal(t, "confirmed", gjson.GetBytes(actual.Config, "credentials.0.state").String())
|
|
assert.Equal(t, "key-without-pin", gjson.GetBytes(actual.Config, "credentials.1.client_key_id").String())
|
|
assert.Equal(t, "iPhone", gjson.GetBytes(actual.Config, "credentials.1.device_name").String())
|
|
})
|
|
}
|
|
|
|
func TestDeleteCredentialOIDCSAMLFromIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
|
|
err := i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "")
|
|
assert.Error(t, err)
|
|
err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "does-not-exist")
|
|
assert.Error(t, err)
|
|
err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeSAML, "")
|
|
assert.Error(t, err)
|
|
err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeSAML, "does-not-exist")
|
|
assert.Error(t, err)
|
|
|
|
credentials := map[CredentialsType]Credentials{
|
|
CredentialsTypePassword: {
|
|
Identifiers: []string{"zab", "bar"},
|
|
Type: CredentialsTypePassword,
|
|
Config: sqlxx.JSONRawMessage(`{"some" : "secret"}`),
|
|
},
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"bar:1234", "baz:5678"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "bar", "subject": "1234"}, {"provider": "baz", "subject": "5678"}]}`),
|
|
},
|
|
CredentialsTypeSAML: {
|
|
Type: CredentialsTypeSAML,
|
|
Identifiers: []string{"bar:1234", "baz:5678"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "bar", "subject": "1234"}, {"provider": "baz", "subject": "5678"}]}`),
|
|
},
|
|
CredentialsTypeWebAuthn: {
|
|
Type: CredentialsTypeWebAuthn,
|
|
Identifiers: []string{"foo", "bar"},
|
|
Config: sqlxx.JSONRawMessage(`{"some" : "secret"}`),
|
|
},
|
|
}
|
|
i.Credentials = credentials
|
|
|
|
err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "zab")
|
|
assert.Error(t, err)
|
|
err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "foo")
|
|
assert.Error(t, err)
|
|
err = i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "bar")
|
|
assert.Error(t, err, "matches multiple OIDC credentials")
|
|
|
|
require.NoError(t, i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeOIDC, "bar:1234"))
|
|
|
|
assert.Len(t, i.Credentials, 4)
|
|
|
|
assert.Contains(t, i.Credentials, CredentialsTypePassword)
|
|
assert.EqualValues(t, i.Credentials[CredentialsTypePassword].Identifiers, []string{"zab", "bar"})
|
|
|
|
assert.Contains(t, i.Credentials, CredentialsTypeWebAuthn)
|
|
assert.EqualValues(t, i.Credentials[CredentialsTypeWebAuthn].Identifiers, []string{"foo", "bar"})
|
|
|
|
assert.Contains(t, i.Credentials, CredentialsTypeSAML)
|
|
assert.EqualValues(t, i.Credentials[CredentialsTypeSAML].Identifiers, []string{"bar:1234", "baz:5678"})
|
|
|
|
assert.Contains(t, i.Credentials, CredentialsTypeOIDC)
|
|
|
|
oidc, ok := i.GetCredentials(CredentialsTypeOIDC)
|
|
require.True(t, ok)
|
|
assert.EqualValues(t, oidc.Identifiers, []string{"baz:5678"})
|
|
var cfg CredentialsOIDC
|
|
_, err = i.ParseCredentials(CredentialsTypeOIDC, &cfg)
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, CredentialsOIDC{Providers: []CredentialsOIDCProvider{{Provider: "baz", Subject: "5678"}}}, cfg)
|
|
|
|
require.NoError(t, i.deleteCredentialOIDCSAMLFromIdentity(CredentialsTypeSAML, "baz:5678"))
|
|
|
|
assert.Len(t, i.Credentials, 4)
|
|
assert.Contains(t, i.Credentials, CredentialsTypeOIDC)
|
|
assert.EqualValues(t, i.Credentials[CredentialsTypeOIDC].Identifiers, []string{"baz:5678"})
|
|
|
|
assert.Contains(t, i.Credentials, CredentialsTypeSAML)
|
|
saml, ok := i.GetCredentials(CredentialsTypeSAML)
|
|
require.True(t, ok)
|
|
assert.EqualValues(t, saml.Identifiers, []string{"bar:1234"})
|
|
var samlCfg CredentialsOIDC
|
|
_, err = i.ParseCredentials(CredentialsTypeSAML, &samlCfg)
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, CredentialsOIDC{Providers: []CredentialsOIDCProvider{{Provider: "bar", Subject: "1234"}}}, samlCfg)
|
|
}
|
|
|
|
func TestMergeOIDCCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
identity *Identity
|
|
newCredentials Credentials
|
|
|
|
expectedIdentity *Identity
|
|
assertErr assert.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "adds OIDC credential if not exists",
|
|
identity: &Identity{},
|
|
newCredentials: Credentials{
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"oidc","subject":"1234"}]}`),
|
|
},
|
|
|
|
expectedIdentity: &Identity{
|
|
Credentials: map[CredentialsType]Credentials{
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers":[{"provider":"oidc","subject":"1234"}]}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "merges OIDC credential if exists",
|
|
identity: &Identity{
|
|
Credentials: map[CredentialsType]Credentials{
|
|
CredentialsTypePassword: {
|
|
Type: CredentialsTypePassword,
|
|
Identifiers: []string{"user@example.com"},
|
|
},
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"foo", "replace:1234", "bar", "baz", "replace:abc", "replace:dont-replace"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [
|
|
{"provider": "replace", "subject": "1234", "use_auto_link": true},
|
|
{"provider": "dont-touch", "subject": "foo"},
|
|
{"provider": "replace", "subject": "abc", "use_auto_link": true},
|
|
{"provider": "also-dont-touch", "subject": "bar", "use_auto_link": true},
|
|
{"provider": "replace", "subject": "dont-replace", "use_auto_link": false}
|
|
]}`),
|
|
},
|
|
},
|
|
},
|
|
newCredentials: Credentials{
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "replace", "subject": "new-subject"}]}`),
|
|
},
|
|
|
|
expectedIdentity: &Identity{
|
|
Credentials: map[CredentialsType]Credentials{
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"foo", "bar", "baz", "replace:dont-replace", "replace:new-subject"},
|
|
Config: sqlxx.JSONRawMessage(`{
|
|
"providers" : [ {
|
|
"subject" : "foo",
|
|
"provider" : "dont-touch",
|
|
"initial_id_token" : "",
|
|
"initial_access_token" : "",
|
|
"initial_refresh_token" : ""
|
|
}, {
|
|
"subject" : "bar",
|
|
"provider" : "also-dont-touch",
|
|
"initial_id_token" : "",
|
|
"initial_access_token" : "",
|
|
"initial_refresh_token" : "",
|
|
"use_auto_link": true
|
|
}, {
|
|
"subject" : "dont-replace",
|
|
"provider" : "replace",
|
|
"initial_id_token" : "",
|
|
"initial_access_token" : "",
|
|
"initial_refresh_token" : ""
|
|
}, {
|
|
"subject" : "new-subject",
|
|
"provider" : "replace",
|
|
"initial_id_token" : "",
|
|
"initial_access_token" : "",
|
|
"initial_refresh_token" : ""
|
|
} ]
|
|
}`),
|
|
},
|
|
CredentialsTypePassword: {
|
|
Type: CredentialsTypePassword,
|
|
Identifiers: []string{"user@example.com"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "errs if new credential has no provider",
|
|
identity: &Identity{
|
|
Credentials: map[CredentialsType]Credentials{
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "oidc", "subject": "1234"}]}`),
|
|
},
|
|
},
|
|
},
|
|
newCredentials: Credentials{
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": []}`),
|
|
},
|
|
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
name: "errs if identity credentials are invalid",
|
|
identity: &Identity{
|
|
Credentials: map[CredentialsType]Credentials{
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage("invalid"),
|
|
},
|
|
},
|
|
},
|
|
newCredentials: Credentials{
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "replace", "subject": "new-subject"}]}`),
|
|
},
|
|
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
name: "errs if new credential config is invalid",
|
|
identity: &Identity{
|
|
Credentials: map[CredentialsType]Credentials{
|
|
CredentialsTypeOIDC: {
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "oidc", "subject": "1234"}]}`),
|
|
},
|
|
},
|
|
},
|
|
newCredentials: Credentials{
|
|
Type: CredentialsTypeOIDC,
|
|
Identifiers: []string{"oidc:1234"},
|
|
Config: sqlxx.JSONRawMessage(`invalid`),
|
|
},
|
|
|
|
assertErr: assert.Error,
|
|
},
|
|
} {
|
|
t.Run("case="+tc.name, func(t *testing.T) {
|
|
err := tc.identity.MergeOIDCCredentials(CredentialsTypeOIDC, tc.newCredentials)
|
|
|
|
if tc.assertErr != nil {
|
|
tc.assertErr(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if tc.expectedIdentity != nil {
|
|
var buf bytes.Buffer
|
|
require.NoError(t, json.Compact(&buf, tc.expectedIdentity.Credentials[CredentialsTypeOIDC].Config))
|
|
tc.expectedIdentity.UpsertCredentialsConfig(CredentialsTypeOIDC, buf.Bytes(), 0)
|
|
assert.EqualExportedValues(t, tc.expectedIdentity, tc.identity)
|
|
}
|
|
})
|
|
}
|
|
}
|