diff --git a/migrations/20221003041349_add_mfa_schema.up.sql b/migrations/20221003041349_add_mfa_schema.up.sql index 49851f520..e0b43abe1 100644 --- a/migrations/20221003041349_add_mfa_schema.up.sql +++ b/migrations/20221003041349_add_mfa_schema.up.sql @@ -39,6 +39,7 @@ comment on table {{ index .Options "Namespace" }}.mfa_challenges is 'auth: store -- add factor_id and amr claims to session + create table if not exists {{ index .Options "Namespace" }}.mfa_amr_claims( session_id uuid not null, created_at timestamptz not null, diff --git a/models/factor_test.go b/models/factor_test.go index 091f83657..682a7e211 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -64,7 +64,7 @@ func (ts *FactorTestSuite) TestUpdateStatus() { u, err := NewUser("", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "some-secret", FactorUnverifiedState, "") + f, err := NewFactor(u, "A1B2C3", TOTP, FactorUnverifiedState, "some-secret") require.NoError(ts.T(), err) require.NoError(ts.T(), f.UpdateStatus(ts.db, newFactorStatus)) require.Equal(ts.T(), newFactorStatus, f.Status) @@ -75,7 +75,7 @@ func (ts *FactorTestSuite) TestUpdateFriendlyName() { u, err := NewUser("", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "some-secret", FactorUnverifiedState, "") + f, err := NewFactor(u, "A1B2C3", TOTP, FactorUnverifiedState, "some-secret") require.NoError(ts.T(), err) require.NoError(ts.T(), f.UpdateFriendlyName(ts.db, newSimpleName)) require.Equal(ts.T(), newSimpleName, f.FriendlyName) diff --git a/models/sessions.go b/models/sessions.go index 09d4ea7be..4fd432017 100644 --- a/models/sessions.go +++ b/models/sessions.go @@ -31,13 +31,19 @@ func (aal AuthenticatorAssuranceLevel) String() string { } } +// AMREntry represents a method that a user has logged in together with the corresponding time +type AMREntry struct { + Method string `json:"method"` + Timestamp int64 `json:"timestamp"` +} + type Session struct { ID uuid.UUID `json:"-" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` FactorID *uuid.UUID `json:"factor_id" db:"factor_id"` - AMRClaims []AMRClaim `json:"amr_claims" has_many:"amr_claims"` + AMRClaims []AMRClaim `json:"amr_claims,omitempty" has_many:"amr_claims"` AAL string `json:"aal" db:"aal"` } @@ -114,7 +120,6 @@ func LogoutSession(tx *storage.Connection, sessionId uuid.UUID) error { func (s *Session) UpdateAssociatedFactor(tx *storage.Connection, factorID *uuid.UUID) error { s.FactorID = factorID - s.AAL = AAL2.String() return tx.Update(s) } @@ -123,3 +128,15 @@ func (s *Session) UpdateAAL(tx *storage.Connection, aal string) error { s.AAL = aal return tx.Update(s) } + +func (s *Session) CalculateAALAndAMR() (aal string, amr []AMREntry) { + amr, aal = []AMREntry{}, AAL1.String() + for _, claim := range s.AMRClaims { + if claim.AuthenticationMethod == TOTPSignIn.String() { + aal = AAL2.String() + } + amr = append(amr, AMREntry{Method: claim.AuthenticationMethod, Timestamp: claim.UpdatedAt.Unix()}) + + } + return aal, amr +} diff --git a/models/sessions_test.go b/models/sessions_test.go new file mode 100644 index 000000000..f3f37f7ee --- /dev/null +++ b/models/sessions_test.go @@ -0,0 +1,63 @@ +package models + +import ( + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/storage" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "time" +) + +type SessionsTestSuite struct { + suite.Suite + db *storage.Connection + Config *conf.GlobalConfiguration +} + +func (ts *SessionsTestSuite) SetupTest() { + TruncateAll(ts.db) + email := "test@example.com" + user, err := NewUser("", email, "secret", "test", nil) + require.NoError(ts.T(), err) + + err = ts.db.Create(user) + require.NoError(ts.T(), err) +} + +func (ts *SessionsTestSuite) TestCalculateAALAndAMR() { + totalDistinctClaims := 2 + u, err := FindUserByEmailAndAudience(ts.db, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + session, err := CreateSession(ts.db, u, &uuid.Nil) + require.NoError(ts.T(), err) + + err = AddClaimToSession(ts.db, session, PasswordGrant) + require.NoError(ts.T(), err) + + firstClaimAddedTime := time.Now().Unix() + err = AddClaimToSession(ts.db, session, TOTPSignIn) + require.NoError(ts.T(), err) + + aal, amr := session.CalculateAALAndAMR() + + require.Equal(ts.T(), AAL2.String(), aal) + require.Equal(ts.T(), totalDistinctClaims, len(amr)) + + err = AddClaimToSession(ts.db, session, TOTPSignIn) + require.NoError(ts.T(), err) + + aal, amr = session.CalculateAALAndAMR() + + require.Equal(ts.T(), AAL2.String(), aal) + require.Equal(ts.T(), totalDistinctClaims, len(amr)) + found := false + for _, claim := range session.AMRClaims { + if claim.AuthenticationMethod == TOTPSignIn.String() { + require.True(ts.T(), firstClaimAddedTime < claim.UpdatedAt.Unix()) + found = true + } + } + require.True(ts.T(), found) + +}