@@ -2,6 +2,7 @@ package models
22
33import (
44 "encoding/json"
5+ "fmt"
56 "testing"
67
78 "github.com/gofrs/uuid"
@@ -31,6 +32,31 @@ func TestFactor(t *testing.T) {
3132 suite .Run (t , ts )
3233}
3334
35+ // TestAMRMethodForFactorType pins the factor-type -> AMR method mapping.
36+ func TestAMRMethodForFactorType (t * testing.T ) {
37+ for factorType , want := range map [string ]AuthenticationMethod {
38+ TOTP : TOTPSignIn ,
39+ Phone : MFAPhone ,
40+ WebAuthn : MFAWebAuthn ,
41+ } {
42+ got , err := amrMethodForFactorType (factorType )
43+ require .NoError (t , err , "factor type %q must map" , factorType )
44+ require .Equal (t , want .String (), got )
45+ }
46+
47+ _ , err := amrMethodForFactorType ("not-a-real-factor-type" )
48+ require .Error (t , err )
49+ }
50+
51+ // TestAuthenticationMethodRoundTrip guards the String() <-> ParseAuthenticationMethod symmetry.
52+ func TestAuthenticationMethodRoundTrip (t * testing.T ) {
53+ for _ , m := range []AuthenticationMethod {TOTPSignIn , MFAPhone , MFAWebAuthn } {
54+ parsed , err := ParseAuthenticationMethod (m .String ())
55+ require .NoError (t , err , "method %q must round-trip" , m .String ())
56+ require .Equal (t , m , parsed )
57+ }
58+ }
59+
3460func (ts * FactorTestSuite ) SetupTest () {
3561 TruncateAll (ts .db )
3662 user , err := NewUser ("" , "agenericemail@gmail.com" , "secret" , "test" , nil )
@@ -72,3 +98,69 @@ func (ts *FactorTestSuite) TestEncodedFactorDoesNotLeakSecret() {
7298 json .Unmarshal (encodedFactor , & decodedFactor )
7399 require .Equal (ts .T (), decodedFactor .Secret , "" )
74100}
101+
102+ // TestDowngradeSessionsToAAL1RemovesAMRClaim asserts that unenrolling a verified
103+ // factor strips the AMR claim it granted, dropping the session back to AAL1.
104+ func (ts * FactorTestSuite ) TestDowngradeSessionsToAAL1RemovesAMRClaim () {
105+ cases := []struct {
106+ desc string
107+ newFactor func (u * User ) * Factor
108+ authMethod AuthenticationMethod
109+ }{
110+ {
111+ desc : "phone" ,
112+ newFactor : func (u * User ) * Factor { return NewPhoneFactor (u , "+15555555555" , "" ) },
113+ authMethod : MFAPhone ,
114+ },
115+ {
116+ desc : "webauthn" ,
117+ newFactor : func (u * User ) * Factor { return NewWebAuthnFactor (u , "webauthnfactor" ) },
118+ authMethod : MFAWebAuthn ,
119+ },
120+ {
121+ desc : "totp" ,
122+ newFactor : func (u * User ) * Factor { return NewTOTPFactor (u , "totpfactor" ) },
123+ authMethod : TOTPSignIn ,
124+ },
125+ }
126+
127+ for i , c := range cases {
128+ ts .Run (c .desc , func () {
129+ user , err := NewUser ("" , fmt .Sprintf ("downgrade-%d@example.com" , i ), "secret" , "test" , nil )
130+ require .NoError (ts .T (), err )
131+ require .NoError (ts .T (), ts .db .Create (user ))
132+
133+ factor := c .newFactor (user )
134+ require .NoError (ts .T (), factor .SetSecret ("secretkey" , false , "" , "" ))
135+ require .NoError (ts .T (), ts .db .Create (factor ))
136+ require .NoError (ts .T (), factor .UpdateStatus (ts .db , FactorStateVerified ))
137+
138+ session , err := NewSession (user .ID , & factor .ID )
139+ require .NoError (ts .T (), err )
140+ require .NoError (ts .T (), ts .db .Create (session ))
141+ require .NoError (ts .T (), AddClaimToSession (ts .db , session .ID , c .authMethod ))
142+ require .NoError (ts .T (), session .UpdateAALAndAssociatedFactor (ts .db , AAL2 , & factor .ID ))
143+
144+ // the claim upgrades the session to AAL2.
145+ loaded , err := FindSessionByID (ts .db , session .ID , false )
146+ require .NoError (ts .T (), err )
147+ aal , _ , err := loaded .CalculateAALAndAMR (user )
148+ require .NoError (ts .T (), err )
149+ require .Equal (ts .T (), AAL2 , aal )
150+
151+ require .NoError (ts .T (), factor .DowngradeSessionsToAAL1 (ts .db ))
152+
153+ downgraded , err := FindSessionByID (ts .db , session .ID , false )
154+ require .NoError (ts .T (), err )
155+ require .Equal (ts .T (), AAL1 .String (), downgraded .GetAAL ())
156+ require .Nil (ts .T (), downgraded .FactorID )
157+
158+ aal , _ , err = downgraded .CalculateAALAndAMR (user )
159+ require .NoError (ts .T (), err )
160+ require .Equal (ts .T (), AAL1 , aal )
161+ for _ , claim := range downgraded .AMRClaims {
162+ require .NotEqual (ts .T (), c .authMethod .String (), claim .GetAuthenticationMethod ())
163+ }
164+ })
165+ }
166+ }
0 commit comments