315 lines
9.5 KiB
Go
315 lines
9.5 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/supabase/auth/internal/api/provider"
|
|
"github.com/supabase/auth/internal/conf"
|
|
"github.com/supabase/auth/internal/storage"
|
|
"github.com/supabase/auth/internal/storage/test"
|
|
)
|
|
|
|
type AccountLinkingTestSuite struct {
|
|
suite.Suite
|
|
|
|
config *conf.GlobalConfiguration
|
|
db *storage.Connection
|
|
}
|
|
|
|
func (ts *AccountLinkingTestSuite) SetupTest() {
|
|
TruncateAll(ts.db)
|
|
}
|
|
|
|
func TestAccountLinking(t *testing.T) {
|
|
globalConfig, err := conf.LoadGlobal(modelsTestConfig)
|
|
require.NoError(t, err)
|
|
|
|
conn, err := test.SetupDBConnection(globalConfig)
|
|
require.NoError(t, err)
|
|
|
|
ts := &AccountLinkingTestSuite{
|
|
config: globalConfig,
|
|
db: conn,
|
|
}
|
|
defer ts.db.Close()
|
|
|
|
suite.Run(t, ts)
|
|
}
|
|
|
|
func (ts *AccountLinkingTestSuite) TestCreateAccountDecisionNoAccounts() {
|
|
// when there are no accounts in the system -- conventional provider
|
|
testEmail := provider.Email{
|
|
Email: "test@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
}
|
|
decision, err := DetermineAccountLinking(ts.db, ts.config, []provider.Email{testEmail}, ts.config.JWT.Aud, "provider", "abcdefgh")
|
|
require.NoError(ts.T(), err)
|
|
|
|
require.Equal(ts.T(), decision.Decision, CreateAccount)
|
|
|
|
// when there are no accounts in the system -- SSO provider
|
|
decision, err = DetermineAccountLinking(ts.db, ts.config, []provider.Email{testEmail}, ts.config.JWT.Aud, "sso:f06f9e3d-ff92-4c47-a179-7acf1fda6387", "abcdefgh")
|
|
require.NoError(ts.T(), err)
|
|
|
|
require.Equal(ts.T(), decision.Decision, CreateAccount)
|
|
}
|
|
|
|
func (ts *AccountLinkingTestSuite) TestCreateAccountDecisionWithAccounts() {
|
|
userA, err := NewUser("", "test@example.com", "", "authenticated", nil)
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(userA))
|
|
identityA, err := NewIdentity(userA, "provider", map[string]interface{}{
|
|
"sub": userA.ID.String(),
|
|
"email": "test@example.com",
|
|
})
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(identityA))
|
|
|
|
userB, err := NewUser("", "test@samltest.id", "", "authenticated", nil)
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(userB))
|
|
|
|
ssoProvider := "sso:f06f9e3d-ff92-4c47-a179-7acf1fda6387"
|
|
identityB, err := NewIdentity(userB, ssoProvider, map[string]interface{}{
|
|
"sub": userB.ID.String(),
|
|
"email": "test@samltest.id",
|
|
})
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(identityB))
|
|
|
|
// when the email doesn't exist in the system -- conventional provider
|
|
decision, err := DetermineAccountLinking(ts.db, ts.config, []provider.Email{
|
|
{
|
|
Email: "other@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
}, ts.config.JWT.Aud, "provider", "abcdefgh")
|
|
require.NoError(ts.T(), err)
|
|
|
|
require.Equal(ts.T(), decision.Decision, CreateAccount)
|
|
require.Equal(ts.T(), decision.LinkingDomain, "default")
|
|
|
|
// when looking for an email that doesn't exist in the SSO linking domain
|
|
decision, err = DetermineAccountLinking(ts.db, ts.config, []provider.Email{
|
|
{
|
|
Email: "other@samltest.id",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
}, ts.config.JWT.Aud, ssoProvider, "abcdefgh")
|
|
require.NoError(ts.T(), err)
|
|
|
|
require.Equal(ts.T(), decision.Decision, CreateAccount)
|
|
require.Equal(ts.T(), decision.LinkingDomain, ssoProvider)
|
|
}
|
|
|
|
func (ts *AccountLinkingTestSuite) TestAccountExists() {
|
|
userA, err := NewUser("", "test@example.com", "", "authenticated", nil)
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(userA))
|
|
identityA, err := NewIdentity(userA, "provider", map[string]interface{}{
|
|
"sub": userA.ID.String(),
|
|
"email": "test@example.com",
|
|
})
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(identityA))
|
|
|
|
decision, err := DetermineAccountLinking(ts.db, ts.config, []provider.Email{
|
|
{
|
|
Email: "test@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
}, ts.config.JWT.Aud, "provider", userA.ID.String())
|
|
require.NoError(ts.T(), err)
|
|
|
|
require.Equal(ts.T(), decision.Decision, AccountExists)
|
|
require.Equal(ts.T(), decision.User.ID, userA.ID)
|
|
}
|
|
|
|
func (ts *AccountLinkingTestSuite) TestLinkingScenarios() {
|
|
userA, err := NewUser("", "test@example.com", "", "authenticated", nil)
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(userA))
|
|
identityA, err := NewIdentity(userA, "provider", map[string]interface{}{
|
|
"sub": userA.ID.String(),
|
|
"email": "test@example.com",
|
|
})
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(identityA))
|
|
|
|
userB, err := NewUser("", "test@samltest.id", "", "authenticated", nil)
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(userB))
|
|
|
|
identityB, err := NewIdentity(userB, "sso:f06f9e3d-ff92-4c47-a179-7acf1fda6387", map[string]interface{}{
|
|
"sub": userB.ID.String(),
|
|
"email": "test@samltest.id",
|
|
})
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(identityB))
|
|
|
|
cases := []struct {
|
|
desc string
|
|
email provider.Email
|
|
sub string
|
|
provider string
|
|
decision AccountLinkingResult
|
|
}{
|
|
{
|
|
// link decision because the below described identity is in the default linking domain but uses "other-provider" instead of "provder"
|
|
desc: "same email address",
|
|
email: provider.Email{
|
|
Email: "test@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
sub: userA.ID.String(),
|
|
provider: "other-provider",
|
|
decision: AccountLinkingResult{
|
|
Decision: LinkAccount,
|
|
User: userA,
|
|
LinkingDomain: "default",
|
|
CandidateEmail: provider.Email{
|
|
Email: "test@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "same email address in uppercase",
|
|
email: provider.Email{
|
|
Email: "TEST@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
sub: userA.ID.String(),
|
|
provider: "other-provider",
|
|
decision: AccountLinkingResult{
|
|
Decision: LinkAccount,
|
|
User: userA,
|
|
LinkingDomain: "default",
|
|
CandidateEmail: provider.Email{
|
|
// expected email should be case insensitive
|
|
Email: "test@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "no link decision because the SSO linking domain is scoped to the provider unique ID",
|
|
email: provider.Email{
|
|
Email: "test@samltest.id",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
sub: userB.ID.String(),
|
|
provider: "sso:f06f9e3d-ff92-4c47-a179-7acf1fda6387",
|
|
// decision: AccountExists,
|
|
decision: AccountLinkingResult{
|
|
Decision: AccountExists,
|
|
User: userB,
|
|
LinkingDomain: "sso:f06f9e3d-ff92-4c47-a179-7acf1fda6387",
|
|
CandidateEmail: provider.Email{
|
|
Email: "test@samltest.id",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "create account with empty email because email is unverified and user exists",
|
|
email: provider.Email{
|
|
Email: "test@example.com",
|
|
Verified: false,
|
|
Primary: true,
|
|
},
|
|
sub: userA.ID.String(),
|
|
provider: "other-provider",
|
|
decision: AccountLinkingResult{
|
|
Decision: CreateAccount,
|
|
LinkingDomain: "default",
|
|
CandidateEmail: provider.Email{
|
|
Email: "",
|
|
Verified: false,
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "create account because email is unverified and user doesn't exist",
|
|
email: provider.Email{
|
|
Email: "other@example.com",
|
|
Verified: false,
|
|
Primary: true,
|
|
},
|
|
sub: "000000000",
|
|
provider: "other-provider",
|
|
decision: AccountLinkingResult{
|
|
Decision: CreateAccount,
|
|
LinkingDomain: "default",
|
|
CandidateEmail: provider.Email{
|
|
Email: "other@example.com",
|
|
Verified: false,
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
ts.Run(c.desc, func() {
|
|
decision, err := DetermineAccountLinking(ts.db, ts.config, []provider.Email{c.email}, ts.config.JWT.Aud, c.provider, c.sub)
|
|
require.NoError(ts.T(), err)
|
|
require.Equal(ts.T(), c.decision.Decision, decision.Decision)
|
|
require.Equal(ts.T(), c.decision.LinkingDomain, decision.LinkingDomain)
|
|
require.Equal(ts.T(), c.decision.CandidateEmail.Email, decision.CandidateEmail.Email)
|
|
require.Equal(ts.T(), c.decision.CandidateEmail.Verified, decision.CandidateEmail.Verified)
|
|
require.Equal(ts.T(), c.decision.CandidateEmail.Primary, decision.CandidateEmail.Primary)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func (ts *AccountLinkingTestSuite) TestMultipleAccounts() {
|
|
userA, err := NewUser("", "test@example.com", "", "authenticated", nil)
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(userA))
|
|
identityA, err := NewIdentity(userA, "provider", map[string]interface{}{
|
|
"sub": userA.ID.String(),
|
|
"email": "test@example.com",
|
|
})
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(identityA))
|
|
|
|
userB, err := NewUser("", "test-b@example.com", "", "authenticated", nil)
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(userB))
|
|
identityB, err := NewIdentity(userB, "provider", map[string]interface{}{
|
|
"sub": userB.ID.String(),
|
|
"email": "test@example.com", // intentionally same as userA
|
|
})
|
|
require.NoError(ts.T(), err)
|
|
require.NoError(ts.T(), ts.db.Create(identityB))
|
|
|
|
// decision is multiple accounts because there are two distinct
|
|
// identities in the same "default" linking domain with the same email
|
|
// address pointing to two different user accounts
|
|
decision, err := DetermineAccountLinking(ts.db, ts.config, []provider.Email{
|
|
{
|
|
Email: "test@example.com",
|
|
Verified: true,
|
|
Primary: true,
|
|
},
|
|
}, ts.config.JWT.Aud, "provider", "abcdefgh")
|
|
require.NoError(ts.T(), err)
|
|
|
|
require.Equal(ts.T(), decision.Decision, MultipleAccounts)
|
|
}
|