Files
upstream-ory-kratos/identity/identity_test.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

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)
}
})
}
}