feat: Add more tests for sessions and edge case

Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
This commit is contained in:
maksim.nabokikh
2026-04-08 17:35:37 +02:00
parent 95fefb46b2
commit 2fb5d78ab7
5 changed files with 702 additions and 9 deletions
+2 -2
View File
@@ -449,8 +449,8 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
return
}
if redirectURL != "" {
// Session found but consent required — no UI allowed.
s.redirectWithError(w, r, authReq, errInteractionRequired, "Consent required")
// Session found but user interaction is needed (consent or MFA) — no UI allowed.
s.redirectWithError(w, r, authReq, errInteractionRequired, "User interaction required")
return
}
return
+55
View File
@@ -908,6 +908,61 @@ func TestScopesCoveredByConsent(t *testing.T) {
}
}
// TestConsentSurvivesSessionDeletion verifies that UserIdentity.Consents
// persists independently from AuthSession lifecycle (logout should not
// clear consent decisions).
func TestConsentSurvivesSessionDeletion(t *testing.T) {
ctx := t.Context()
httpServer, s := newTestServerWithSessions(t, nil)
defer httpServer.Close()
userID := "test-user"
connectorID := "mock"
clientID := "test-client"
// Create UserIdentity with existing consents.
require.NoError(t, s.storage.CreateUserIdentity(ctx, storage.UserIdentity{
UserID: userID,
ConnectorID: connectorID,
Claims: storage.Claims{UserID: userID, Username: "testuser"},
Consents: map[string][]string{clientID: {"openid", "email", "profile"}},
CreatedAt: time.Now(),
LastLogin: time.Now(),
}))
// Create and then delete the session (simulating logout).
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID, ConnectorID: connectorID, Nonce: "nonce",
CreatedAt: time.Now(), LastActivity: time.Now(),
}))
require.NoError(t, s.storage.DeleteAuthSession(ctx, userID, connectorID))
// Session is gone.
_, err := s.storage.GetAuthSession(ctx, userID, connectorID)
require.ErrorIs(t, err, storage.ErrNotFound)
// Consent survives.
ui, err := s.storage.GetUserIdentity(ctx, userID, connectorID)
require.NoError(t, err)
require.Equal(t, []string{"openid", "email", "profile"}, ui.Consents[clientID],
"consent should survive session deletion")
}
// TestConsentIsolatedBetweenClients verifies that consent given for
// client-A does not satisfy scope check for client-B.
func TestConsentIsolatedBetweenClients(t *testing.T) {
approvedForA := map[string][]string{"client-a": {"openid", "email"}}
// client-b should not have consent.
require.False(t, scopesCoveredByConsent(approvedForA["client-b"], []string{"openid", "email"}),
"consent for client-a should not cover client-b")
// client-a should have consent.
require.True(t, scopesCoveredByConsent(approvedForA["client-a"], []string{"openid", "email"}),
"consent for client-a should cover client-a's requested scopes")
}
func TestHandlePasswordLoginWithSkipApproval(t *testing.T) {
ctx := t.Context()
+56
View File
@@ -293,6 +293,62 @@ func TestDiscoveryWithoutSessions(t *testing.T) {
require.Empty(t, d.EndSession)
}
// TestHandleLogoutFromCookie tests logout without id_token_hint,
// where the user is identified by their session cookie alone.
func TestHandleLogoutFromCookie(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := t.Context()
userID := "test-user"
connectorID := "mock"
nonce := "testnonce"
require.NoError(t, server.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID, ConnectorID: connectorID, Nonce: nonce,
CreatedAt: time.Now(), LastActivity: time.Now(),
}))
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/logout", nil)
req.AddCookie(&http.Cookie{
Name: "dex_session",
Value: sessionCookieValue(userID, connectorID, nonce, server.sessionConfig.CookieEncryptionKey),
})
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Contains(t, rr.Body.String(), "successfully logged out")
// Session should be deleted.
_, err := server.storage.GetAuthSession(ctx, userID, connectorID)
require.ErrorIs(t, err, storage.ErrNotFound)
// Cookie should be cleared.
for _, c := range rr.Result().Cookies() {
if c.Name == "dex_session" {
require.Equal(t, -1, c.MaxAge)
}
}
}
// TestLogoutCallbackWithExpiredSession tests that /logout/callback
// returns an error when the session has expired or been deleted.
func TestLogoutCallbackWithExpiredSession(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
// No session created — cookie points to nonexistent session.
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/logout/callback", nil)
req.AddCookie(&http.Cookie{
Name: "dex_session",
Value: sessionCookieValue("user-1", "mock", "nonce", server.sessionConfig.CookieEncryptionKey),
})
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRevokeRefreshTokensReturnsConnectorData(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
+5 -7
View File
@@ -336,13 +336,11 @@ func TestRefreshTokenAuthTime(t *testing.T) {
"access token should not have auth_time when sessions are disabled")
}
// TODO: newIDToken in handleRefreshToken is currently called with time.Time{},
// so the ID token does not include auth_time. Once fixed, uncomment:
// if tc.wantAuthTime {
// idClaims := decodeJWTClaims(t, resp.IDToken)
// assert.Equal(t, float64(loginTime.Unix()), idClaims["auth_time"],
// "id token auth_time should match UserIdentity.LastLogin")
// }
if tc.wantAuthTime {
idClaims := decodeJWTClaims(t, resp.IDToken)
assert.Equal(t, float64(loginTime.Unix()), idClaims["auth_time"],
"id token auth_time should match UserIdentity.LastLogin")
}
})
}
}
File diff suppressed because it is too large Load Diff