From 253db82d39b55067f3af611c2eb5b9c331dfba2a Mon Sep 17 00:00:00 2001 From: martin el cheikh Date: Sat, 12 Sep 2026 17:57:47 -0300 Subject: [PATCH] fix(models): exclude oauth client sessions from mfa step-up invalidation (#2801) --- internal/models/sessions.go | 2 +- internal/models/sessions_test.go | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/internal/models/sessions.go b/internal/models/sessions.go index a025557b54..00fd6172f7 100644 --- a/internal/models/sessions.go +++ b/internal/models/sessions.go @@ -351,7 +351,7 @@ func updateFactorAssociatedSessions(tx *storage.Connection, userID, factorID uui } func InvalidateSessionsWithAALLessThan(tx *storage.Connection, userID uuid.UUID, level string) error { - return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND aal < ?", userID, level).Exec() + return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND aal < ? AND oauth_client_id IS NULL", userID, level).Exec() } // Logout deletes all sessions for a user. diff --git a/internal/models/sessions_test.go b/internal/models/sessions_test.go index 77794874c8..2830137a87 100644 --- a/internal/models/sessions_test.go +++ b/internal/models/sessions_test.go @@ -57,6 +57,58 @@ func (ts *SessionsTestSuite) TestFindBySessionIDWithForUpdate() { require.Equal(ts.T(), session.ID, found.ID) } +func (ts *SessionsTestSuite) TestInvalidateSessionsWithAALLessThan_PreservesOAuthSessions() { + u, err := FindUserByEmailAndAudience(ts.db, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + // Standard AAL1 browser session + browserSession, err := NewSession(u.ID, nil) + require.NoError(ts.T(), err) + require.NoError(ts.T(), ts.db.Create(browserSession)) + + // OAuth client session (also AAL1, but has oauth_client_id set) + clientName := "OAuth App" + client := &OAuthServerClient{ + ID: uuid.Must(uuid.NewV4()), + ClientName: &clientName, + RegistrationType: "dynamic", + ClientType: OAuthServerClientTypeConfidential, + ClientSecretHash: "hash", + RedirectURIs: "https://example.com/callback", + GrantTypes: "authorization_code,refresh_token", + TokenEndpointAuthMethod: TokenEndpointAuthMethodClientSecretBasic, + } + require.NoError(ts.T(), CreateOAuthServerClient(ts.db, client)) + + oauthSession, err := NewSession(u.ID, nil) + require.NoError(ts.T(), err) + oauthSession.OAuthClientID = &client.ID + require.NoError(ts.T(), ts.db.Create(oauthSession)) + + // Higher-assurance AAL2 session + aal2Session, err := NewSession(u.ID, nil) + require.NoError(ts.T(), err) + aal2Session.AAL = AAL2.PointerString() + require.NoError(ts.T(), ts.db.Create(aal2Session)) + + // Step-up verification invalidates lower-AAL sessions + require.NoError(ts.T(), InvalidateSessionsWithAALLessThan(ts.db, u.ID, AAL2.String())) + + // Browser AAL1 session should be invalidated + _, err = FindSessionByID(ts.db, browserSession.ID, false) + require.ErrorIs(ts.T(), err, SessionNotFoundError{}) + + // OAuth client session must be preserved + foundOAuth, err := FindSessionByID(ts.db, oauthSession.ID, false) + require.NoError(ts.T(), err) + require.Equal(ts.T(), oauthSession.ID, foundOAuth.ID) + + // AAL2 session must be preserved + foundAAL2, err := FindSessionByID(ts.db, aal2Session.ID, false) + require.NoError(ts.T(), err) + require.Equal(ts.T(), aal2Session.ID, foundAAL2.ID) +} + func (ts *SessionsTestSuite) AddClaimAndReloadSession(session *Session, claim AuthenticationMethod) *Session { err := AddClaimToSession(ts.db, session.ID, claim) require.NoError(ts.T(), err)