Skip to content

Commit 2ed998d

Browse files
committed
chore: limit aal1 sessions correctly
1 parent c7b58be commit 2ed998d

2 files changed

Lines changed: 121 additions & 1 deletion

File tree

internal/tokens/service.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,8 +669,18 @@ func (s *Service) GenerateAccessToken(r *http.Request, tx *storage.Connection, p
669669
return "", 0, terr
670670
}
671671

672+
var expiresAt time.Time
673+
672674
issuedAt := s.now().UTC()
673-
expiresAt := issuedAt.Add(time.Second * time.Duration(config.JWT.Exp))
675+
if config.Sessions.AllowLowAAL != nil && *config.Sessions.AllowLowAAL != 0 && models.CompareAAL(aal, params.User.HighestPossibleAAL()) < 0 {
676+
// if user has mfa enabled and the session has not yet been upgraded
677+
// and Limit duration of AAL1 sessions is enabled
678+
// expiresAt should be set to the maximum duration for low aal sessions
679+
expiresAt = issuedAt.Add(*config.Sessions.AllowLowAAL)
680+
} else {
681+
expiresAt = issuedAt.Add(time.Second * time.Duration(config.JWT.Exp))
682+
}
683+
674684
var clientID string
675685
if params.ClientID != nil && *params.ClientID != uuid.Nil {
676686
clientID = params.ClientID.String()

internal/tokens/service_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,3 +1192,113 @@ func TestAsRedirectURL(t *testing.T) {
11921192
require.Contains(t, fragment, "sb", "Fragment should contain Supabase Auth identifier 'sb'")
11931193
require.Equal(t, "", fragment.Get("sb"), "Supabase Auth identifier should have empty value")
11941194
}
1195+
1196+
func TestGenerateAccessTokenAllowLowAAL(t *testing.T) {
1197+
config, err := conf.LoadGlobal("../../hack/test.env")
1198+
require.NoError(t, err)
1199+
1200+
conn, err := test.SetupDBConnection(config)
1201+
require.NoError(t, err)
1202+
defer conn.Close()
1203+
1204+
allowLowAAL := 5 * time.Minute
1205+
1206+
req, err := http.NewRequest("POST", "https://example.com/", nil)
1207+
require.NoError(t, err)
1208+
1209+
now := time.Now().UTC().Truncate(time.Second)
1210+
1211+
t.Run("AAL1 session for MFA user uses AllowLowAAL expiry", func(t *testing.T) {
1212+
models.TruncateAll(conn)
1213+
1214+
u, err := models.NewUser("", "test@example.com", "password", "authenticated", nil)
1215+
require.NoError(t, err)
1216+
require.NoError(t, conn.Create(u))
1217+
1218+
// Add a verified TOTP factor so HighestPossibleAAL() returns AAL2
1219+
factor := models.NewFactor(u, "my-totp", models.TOTP, models.FactorStateVerified)
1220+
require.NoError(t, conn.Create(factor))
1221+
require.NoError(t, conn.Eager().Find(u, u.ID))
1222+
1223+
session, err := models.NewSession(u.ID, nil)
1224+
require.NoError(t, err)
1225+
// Session stays at AAL1 (default)
1226+
require.NoError(t, conn.Create(session))
1227+
1228+
cfg := *config
1229+
cfg.Sessions.AllowLowAAL = &allowLowAAL
1230+
1231+
srv := NewService(&cfg, &panicHookManager{})
1232+
srv.SetTimeFunc(func() time.Time { return now })
1233+
1234+
_, expiresAt, err := srv.GenerateAccessToken(req, conn, GenerateAccessTokenParams{
1235+
User: u,
1236+
SessionID: &session.ID,
1237+
AuthenticationMethod: models.PasswordGrant,
1238+
})
1239+
require.NoError(t, err)
1240+
require.Equal(t, now.Add(allowLowAAL).Unix(), expiresAt)
1241+
})
1242+
1243+
t.Run("AAL2 session for MFA user uses standard JWT expiry", func(t *testing.T) {
1244+
models.TruncateAll(conn)
1245+
1246+
u, err := models.NewUser("", "test2@example.com", "password", "authenticated", nil)
1247+
require.NoError(t, err)
1248+
require.NoError(t, conn.Create(u))
1249+
1250+
factor := models.NewFactor(u, "my-totp", models.TOTP, models.FactorStateVerified)
1251+
require.NoError(t, conn.Create(factor))
1252+
require.NoError(t, conn.Eager().Find(u, u.ID))
1253+
1254+
session, err := models.NewSession(u.ID, &factor.ID)
1255+
require.NoError(t, err)
1256+
aal2 := models.AAL2.String()
1257+
session.AAL = &aal2
1258+
require.NoError(t, conn.Create(session))
1259+
1260+
cfg := *config
1261+
cfg.Sessions.AllowLowAAL = &allowLowAAL
1262+
1263+
srv := NewService(&cfg, &panicHookManager{})
1264+
srv.SetTimeFunc(func() time.Time { return now })
1265+
1266+
_, expiresAt, err := srv.GenerateAccessToken(req, conn, GenerateAccessTokenParams{
1267+
User: u,
1268+
SessionID: &session.ID,
1269+
AuthenticationMethod: models.PasswordGrant,
1270+
})
1271+
require.NoError(t, err)
1272+
require.Equal(t, now.Add(time.Second*time.Duration(config.JWT.Exp)).Unix(), expiresAt)
1273+
})
1274+
1275+
t.Run("AAL1 session without AllowLowAAL uses standard JWT expiry", func(t *testing.T) {
1276+
models.TruncateAll(conn)
1277+
1278+
u, err := models.NewUser("", "test3@example.com", "password", "authenticated", nil)
1279+
require.NoError(t, err)
1280+
require.NoError(t, conn.Create(u))
1281+
1282+
factor := models.NewFactor(u, "my-totp", models.TOTP, models.FactorStateVerified)
1283+
require.NoError(t, conn.Create(factor))
1284+
require.NoError(t, conn.Eager().Find(u, u.ID))
1285+
1286+
session, err := models.NewSession(u.ID, nil)
1287+
require.NoError(t, err)
1288+
require.NoError(t, conn.Create(session))
1289+
1290+
cfg := *config
1291+
cfg.Sessions.AllowLowAAL = nil
1292+
1293+
srv := NewService(&cfg, &panicHookManager{})
1294+
srv.SetTimeFunc(func() time.Time { return now })
1295+
1296+
_, expiresAt, err := srv.GenerateAccessToken(req, conn, GenerateAccessTokenParams{
1297+
User: u,
1298+
SessionID: &session.ID,
1299+
AuthenticationMethod: models.PasswordGrant,
1300+
})
1301+
require.NoError(t, err)
1302+
require.Equal(t, now.Add(time.Second*time.Duration(config.JWT.Exp)).Unix(), expiresAt)
1303+
})
1304+
}

0 commit comments

Comments
 (0)