1
0
forked from baron/baron-sso

세션 종료 시 모든 세션 종료 에러 수정

This commit is contained in:
2026-04-06 14:22:00 +09:00
parent 4ad7518328
commit 890ddd9b3c
2 changed files with 148 additions and 1 deletions

View File

@@ -7219,6 +7219,9 @@ func (h *AuthHandler) loadSessionClientBindings(ctx context.Context, userID stri
logs, err := h.AuditRepo.FindByUserAndEvents(ctx, userID, []string{
"consent.granted",
"POST /api/v1/auth/oidc/login/accept",
"POST /api/v1/auth/password/login",
"password_login_success",
"login_success",
}, 200)
if err != nil {
return bindings
@@ -7265,7 +7268,7 @@ func (h *AuthHandler) revokeHydraSessionAccess(ctx context.Context, userID strin
clientIDs := h.loadSessionClientBindings(ctx, userID)[strings.TrimSpace(sessionID)]
if len(clientIDs) == 0 {
return h.Hydra.RevokeConsentSessions(ctx, userID, "")
return nil
}
for _, clientID := range clientIDs {
if err := h.Hydra.RevokeConsentSessions(ctx, userID, clientID); err != nil {

View File

@@ -430,6 +430,150 @@ func TestDeleteMySession_Success(t *testing.T) {
mockKratos.AssertExpectations(t)
}
func TestDeleteMySession_DoesNotRevokeAllHydraSessionsWhenClientBindingMissing(t *testing.T) {
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
var hydraRevokeCalls int
client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
switch r.URL.Host {
case "kratos.test":
if r.URL.Path == "/sessions/whoami" {
return httpJSONAny(r, http.StatusOK, map[string]any{
"id": "current-sid",
"authenticated_at": time.Now().UTC().Format(time.RFC3339),
"identity": map[string]any{
"id": "user-123",
"traits": map[string]any{
"email": "user@example.com",
"name": "User",
"role": "user",
},
},
}), nil
}
case "hydra.test":
if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
hydraRevokeCalls++
return httpResponse(r, http.StatusNoContent, ""), nil
}
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})}
setDefaultHTTPClientForTest(t, client.Transport)
mockKratos := new(MockKratosAdminService)
mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
{ID: "target-sid", Active: true},
}, nil).Once()
mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{
ID: "target-sid",
Active: true,
}, nil).Once()
mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once()
auditRepo := &mockAuditRepo{}
h := &AuthHandler{
KratosAdmin: mockKratos,
AuditRepo: auditRepo,
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: client,
},
}
app := fiber.New()
app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession)
req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil)
req.Header.Set("Cookie", "ory_kratos_session=valid")
req.Header.Set("User-Agent", "session-test-agent")
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, 0, hydraRevokeCalls)
if assert.Len(t, auditRepo.logs, 1) {
assert.Equal(t, "session.revoked", auditRepo.logs[0].EventType)
assert.Equal(t, "user-123", auditRepo.logs[0].UserID)
assert.Contains(t, auditRepo.logs[0].Details, "target-sid")
}
mockKratos.AssertExpectations(t)
}
func TestDeleteMySession_RevokesHydraClientBoundFromPasswordLoginAudit(t *testing.T) {
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
var hydraRevokeCalls int
var revokedClient string
client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
switch r.URL.Host {
case "kratos.test":
if r.URL.Path == "/sessions/whoami" {
return httpJSONAny(r, http.StatusOK, map[string]any{
"id": "current-sid",
"authenticated_at": time.Now().UTC().Format(time.RFC3339),
"identity": map[string]any{
"id": "user-123",
"traits": map[string]any{
"email": "user@example.com",
"name": "User",
"role": "user",
},
},
}), nil
}
case "hydra.test":
if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
revokedClient = r.URL.Query().Get("client")
hydraRevokeCalls++
return httpResponse(r, http.StatusNoContent, ""), nil
}
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})}
setDefaultHTTPClientForTest(t, client.Transport)
mockKratos := new(MockKratosAdminService)
mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
{ID: "target-sid", Active: true},
}, nil).Once()
mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{
ID: "target-sid",
Active: true,
}, nil).Once()
mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once()
auditRepo := &mockAuditRepo{}
h := &AuthHandler{
KratosAdmin: mockKratos,
AuditRepo: auditRepo,
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: client,
},
}
auditRepo.logs = append(auditRepo.logs, domain.AuditLog{
UserID: "user-123",
EventType: "POST /api/v1/auth/password/login",
SessionID: "target-sid",
Details: `{"client_id":"adminfront","client_name":"AdminFront","session_id":"target-sid"}`,
})
app := fiber.New()
app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession)
req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil)
req.Header.Set("Cookie", "ory_kratos_session=valid")
req.Header.Set("User-Agent", "session-test-agent")
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, 1, hydraRevokeCalls)
assert.Equal(t, "adminfront", revokedClient)
mockKratos.AssertExpectations(t)
}
func TestGetHydraProfile_RejectsInactiveLinkedSession(t *testing.T) {
client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Host == "hydra.test" && r.URL.Path == "/oauth2/introspect" {