diff --git a/client/client.go b/client/client.go index ff730465d90..6c3af3d87a6 100644 --- a/client/client.go +++ b/client/client.go @@ -209,6 +209,11 @@ type Client struct { RegistrationClientURI string `json:"registration_client_uri,omitempty" db:"-"` } +type LoginSessionClient struct { + Client + LoginSessionID string `json:"login_session_id,omitempty" db:"login_session_id"` +} + func (Client) TableName() string { return "hydra_client" } diff --git a/consent/doc.go b/consent/doc.go index eec38a51c94..f9a917ad0e9 100644 --- a/consent/doc.go +++ b/consent/doc.go @@ -56,10 +56,20 @@ type swaggerRevokeConsentSessions struct { // in: query Client string `json:"client"` + // If set, deletes only those consent sessions by the Subject that have been granted to the specified session id. Can be combined with client or all parameter. + // + // in: query + LoginSessionId string `json:"login_session_id"` + // If set to `?all=true`, deletes all consent sessions by the Subject that have been granted. // // in: query All bool `json:"all"` + + // If set to `?trigger_back_channel_logout=true`, performs back channel logout for matching clients + // + // in: query + TriggerBackChannelLogout bool `json:"trigger_back_channel_logout"` } // swagger:parameters listSubjectConsentSessions diff --git a/consent/handler.go b/consent/handler.go index 78a5897d5da..3863f4f669d 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -102,6 +102,9 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin) { func (h *Handler) DeleteConsentSession(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { subject := r.URL.Query().Get("subject") client := r.URL.Query().Get("client") + loginSessionId := r.URL.Query().Get("login_session_id") + triggerBackChannelLogout := r.URL.Query().Get("trigger_back_channel_logout") + allClients := r.URL.Query().Get("all") == "true" if subject == "" { h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'subject' is not defined but should have been.`))) @@ -110,14 +113,40 @@ func (h *Handler) DeleteConsentSession(w http.ResponseWriter, r *http.Request, p switch { case len(client) > 0: - if err := h.r.ConsentManager().RevokeSubjectClientConsentSession(r.Context(), subject, client); err != nil && !errors.Is(err, x.ErrNotFound) { - h.r.Writer().WriteError(w, r, err) - return + if len(loginSessionId) > 0 { + if triggerBackChannelLogout == "true" { + h.r.ConsentStrategy().ExecuteBackChannelLogoutByClientSession(r.Context(), r, subject, client, loginSessionId) + } + if err := h.r.ConsentManager().RevokeSubjectClientLoginSessionConsentSession(r.Context(), subject, client, loginSessionId); err != nil && !errors.Is(err, x.ErrNotFound) { + h.r.Writer().WriteError(w, r, err) + return + } + } else { + if triggerBackChannelLogout == "true" { + h.r.ConsentStrategy().ExecuteBackChannelLogoutByClient(r.Context(), r, subject, client) + } + if err := h.r.ConsentManager().RevokeSubjectClientConsentSession(r.Context(), subject, client); err != nil && !errors.Is(err, x.ErrNotFound) { + h.r.Writer().WriteError(w, r, err) + return + } } case allClients: - if err := h.r.ConsentManager().RevokeSubjectConsentSession(r.Context(), subject); err != nil && !errors.Is(err, x.ErrNotFound) { - h.r.Writer().WriteError(w, r, err) - return + if len(loginSessionId) > 0 { + if triggerBackChannelLogout == "true" { + h.r.ConsentStrategy().ExecuteBackChannelLogoutBySession(r.Context(), r, subject, loginSessionId) + } + if err := h.r.ConsentManager().RevokeLoginSessionConsentSession(r.Context(), loginSessionId); err != nil && !errors.Is(err, x.ErrNotFound) { + h.r.Writer().WriteError(w, r, err) + return + } + } else { + if triggerBackChannelLogout == "true" { + h.r.ConsentStrategy().ExecuteBackChannelLogoutBySubject(r.Context(), r, subject) + } + if err := h.r.ConsentManager().RevokeSubjectConsentSession(r.Context(), subject); err != nil && !errors.Is(err, x.ErrNotFound) { + h.r.Writer().WriteError(w, r, err) + return + } } default: h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter both 'client' and 'all' is not defined but one of them should have been.`))) diff --git a/consent/handler_test.go b/consent/handler_test.go index 60fa873b1df..3965536a168 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -21,12 +21,22 @@ package consent_test import ( + "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" + "net/url" + "sync" "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + "github.com/ory/hydra/driver" + "github.com/ory/x/sqlxx" "github.com/ory/hydra/x" @@ -213,3 +223,261 @@ func TestGetConsentRequest(t *testing.T) { }) } } + +func TestDeleteConsentSession(t *testing.T) { + newWg := func(add int) *sync.WaitGroup { + var wg sync.WaitGroup + wg.Add(add) + return &wg + } + + t.Run("case=subject=subject-1,client=client-1,session=session-1,trigger_back_channel_logout=true", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(1) + cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1"}, backChannelWG) + performLoginFlow(t, reg, "1", cl) + performLoginFlow(t, reg, "2", cl) + performDeleteConsentSession(t, reg, "client-1", "login-session-1", true) + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.NoError(t, err) + require.NotNil(t, c2) + backChannelWG.Wait() + }) + + t.Run("case=subject=subject-1,client=client-1,session=session-1,trigger_back_channel_logout=false", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(0) + cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG) + performLoginFlow(t, reg, "1", cl) + performLoginFlow(t, reg, "2", cl) + performDeleteConsentSession(t, reg, "client-1", "login-session-1", false) + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.NoError(t, err) + require.NotNil(t, c2) + backChannelWG.Wait() + }) + + t.Run("case=subject=subject-1,client=client-1,trigger_back_channel_logout=true", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(2) + cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1", "login-session-2"}, backChannelWG) + performLoginFlow(t, reg, "1", cl) + performLoginFlow(t, reg, "2", cl) + + performDeleteConsentSession(t, reg, "client-1", nil, true) + + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c2) + backChannelWG.Wait() + }) + + t.Run("case=subject=subject-1,client=client-1,trigger_back_channel_logout=false", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(0) + cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG) + performLoginFlow(t, reg, "1", cl) + performLoginFlow(t, reg, "2", cl) + + performDeleteConsentSession(t, reg, "client-1", nil, false) + + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c2) + backChannelWG.Wait() + }) + + t.Run("case=subject=subject-1,all=true,session=session-1,trigger_back_channel_logout=true", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(1) + cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1"}, backChannelWG) + cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{}, backChannelWG) + performLoginFlow(t, reg, "1", cl1) + performLoginFlow(t, reg, "2", cl2) + + performDeleteConsentSession(t, reg, nil, "login-session-1", true) + + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.NoError(t, err) + require.NotNil(t, c2) + backChannelWG.Wait() + }) + + t.Run("case=subject=subject-1,all=true,session=session-1,trigger_back_channel_logout=false", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(0) + cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG) + cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{}, backChannelWG) + performLoginFlow(t, reg, "1", cl1) + performLoginFlow(t, reg, "2", cl2) + + performDeleteConsentSession(t, reg, nil, "login-session-1", false) + + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.NoError(t, err) + require.NotNil(t, c2) + backChannelWG.Wait() + }) + + t.Run("case=subject=subject-1,all=true,trigger_back_channel_logout=true", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(2) + cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1"}, backChannelWG) + cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{"login-session-2"}, backChannelWG) + performLoginFlow(t, reg, "1", cl1) + performLoginFlow(t, reg, "2", cl2) + + performDeleteConsentSession(t, reg, nil, nil, true) + + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c2) + backChannelWG.Wait() + }) + + t.Run("case=subject=subject-1,all=true,trigger_back_channel_logout=false", func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistryMemory(t, conf) + backChannelWG := newWg(0) + cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG) + cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{}, backChannelWG) + performLoginFlow(t, reg, "1", cl1) + performLoginFlow(t, reg, "2", cl2) + + performDeleteConsentSession(t, reg, nil, nil, false) + + c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c1) + c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2") + require.Error(t, x.ErrNotFound, err) + require.Nil(t, c2) + backChannelWG.Wait() + }) +} + +func performDeleteConsentSession(t *testing.T, reg driver.Registry, client, loginSessionId interface{}, triggerBackChannelLogout bool) { + h := NewHandler(reg, reg.Config()) + r := x.NewRouterAdmin() + h.SetRoutes(r) + ts := httptest.NewServer(r) + defer ts.Close() + c := &http.Client{} + + u, _ := url.Parse(ts.URL + SessionsPath + "/consent") + q := u.Query() + q.Set("subject", "subject-1") + if client != nil && len(client.(string)) != 0 { + q.Set("client", client.(string)) + } else { + q.Set("all", "true") + } + if loginSessionId != nil && len(loginSessionId.(string)) != 0 { + q.Set("login_session_id", loginSessionId.(string)) + } + if triggerBackChannelLogout { + q.Set("trigger_back_channel_logout", "true") + } + u.RawQuery = q.Encode() + req, err := http.NewRequest(http.MethodDelete, u.String(), nil) + + require.NoError(t, err) + _, err = c.Do(req) + require.NoError(t, err) +} + +func performLoginFlow(t *testing.T, reg driver.Registry, flowId string, cl *client.Client) { + subject := "subject-1" + loginSessionId := "login-session-" + flowId + loginChallenge := "login-challenge-" + flowId + consentChallenge := "consent-challenge-" + flowId + + ls := &LoginSession{ + ID: loginSessionId, + Subject: subject, + } + lr := &LoginRequest{ + ID: loginChallenge, + Client: cl, + Verifier: "login-verifier-" + flowId, + } + cr := &ConsentRequest{ + ID: consentChallenge, + Subject: subject, + Client: cl, + LoginChallenge: sqlxx.NullString(loginChallenge), + LoginSessionID: sqlxx.NullString(loginSessionId), + } + hcr := &HandledConsentRequest{ + ConsentRequest: cr, + ID: consentChallenge, + HandledAt: sqlxx.NullTime(time.Now().UTC()), + } + + require.NoError(t, reg.ConsentManager().CreateLoginSession(context.Background(), ls)) + require.NoError(t, reg.ConsentManager().CreateLoginRequest(context.Background(), lr)) + require.NoError(t, reg.ConsentManager().CreateConsentRequest(context.Background(), cr)) + _, err := reg.ConsentManager().HandleConsentRequest(context.Background(), consentChallenge, hcr) + require.NoError(t, err) +} + +func createClientWithBackChannelEndpoint(t *testing.T, reg driver.Registry, clientId string, expectedBackChannelLogoutFlowIds []string, wg *sync.WaitGroup) *client.Client { + return func(t *testing.T, key string, wg *sync.WaitGroup, cb func(t *testing.T, logoutToken gjson.Result)) *client.Client { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer wg.Done() + require.NoError(t, r.ParseForm()) + lt := r.PostFormValue("logout_token") + assert.NotEmpty(t, lt) + token, err := reg.OpenIDJWTStrategy().Decode(r.Context(), lt) + require.NoError(t, err) + var b bytes.Buffer + require.NoError(t, json.NewEncoder(&b).Encode(token.Claims)) + cb(t, gjson.Parse(b.String())) + })) + t.Cleanup(server.Close) + c := &client.Client{ + OutfacingID: clientId, + BackChannelLogoutURI: server.URL, + } + err := reg.ClientManager().CreateClient(context.Background(), c) + require.NoError(t, err) + return c + }(t, clientId, wg, func(t *testing.T, logoutToken gjson.Result) { + sid := logoutToken.Get("sid").String() + assert.Contains(t, expectedBackChannelLogoutFlowIds, sid) + for i, v := range expectedBackChannelLogoutFlowIds { + if v == sid { + expectedBackChannelLogoutFlowIds = append(expectedBackChannelLogoutFlowIds[:i], expectedBackChannelLogoutFlowIds[i+1:]...) + break + } + } + }) +} diff --git a/consent/manager.go b/consent/manager.go index f0fa286050b..a10328b8532 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -42,7 +42,9 @@ type Manager interface { GetConsentRequest(ctx context.Context, challenge string) (*ConsentRequest, error) HandleConsentRequest(ctx context.Context, challenge string, r *HandledConsentRequest) (*ConsentRequest, error) RevokeSubjectConsentSession(ctx context.Context, user string) error + RevokeLoginSessionConsentSession(ctx context.Context, loginSessionId string) error RevokeSubjectClientConsentSession(ctx context.Context, user, client string) error + RevokeSubjectClientLoginSessionConsentSession(ctx context.Context, user, client, loginSessionId string) error VerifyAndInvalidateConsentRequest(ctx context.Context, verifier string) (*HandledConsentRequest, error) FindGrantedAndRememberedConsentRequests(ctx context.Context, client, user string) ([]HandledConsentRequest, error) @@ -64,8 +66,9 @@ type Manager interface { CreateForcedObfuscatedLoginSession(ctx context.Context, session *ForcedObfuscatedLoginSession) error GetForcedObfuscatedLoginSession(ctx context.Context, client, obfuscated string) (*ForcedObfuscatedLoginSession, error) - ListUserAuthenticatedClientsWithFrontChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) - ListUserAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) + ListUserSessionAuthenticatedClientsWithFrontChannelLogout(ctx context.Context, subject, sid string) ([]client.LoginSessionClient, error) + ListUserSessionAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject, sid string) ([]client.LoginSessionClient, error) + ListUserAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject string) ([]client.LoginSessionClient, error) CreateLogoutRequest(ctx context.Context, request *LogoutRequest) error GetLogoutRequest(ctx context.Context, challenge string) (*LogoutRequest, error) diff --git a/consent/manager_test_helpers.go b/consent/manager_test_helpers.go index 960fd50d865..824e982c851 100644 --- a/consent/manager_test_helpers.go +++ b/consent/manager_test_helpers.go @@ -692,14 +692,16 @@ func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.Fosit // The idea of this test is to create two identities (subjects) with 4 sessions each, where // only some sessions have been associated with a client that has a front channel logout url - subjects := make([]string, 1) + subjects := make([]string, 2) for k := range subjects { subjects[k] = fmt.Sprintf("subject-ListUserAuthenticatedClientsWithFrontAndBackChannelLogout-%d", k) } - sessions := make([]LoginSession, len(subjects)*1) - frontChannels := map[string][]client.Client{} - backChannels := map[string][]client.Client{} + sessions := make([]LoginSession, len(subjects)*4) + frontChannels := map[string][]*client.Client{} + backChannels := map[string][]*client.Client{} + backChannelsBySubject := map[string][]*client.Client{} + for k := range sessions { id := uuid.New().String() subject := subjects[k%len(subjects)] @@ -715,15 +717,17 @@ func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.Fosit switch k % 4 { case 0: cl.FrontChannelLogoutURI = "http://some-url.com/" - frontChannels[id] = append(frontChannels[id], *cl) + frontChannels[id] = append(frontChannels[id], cl) case 1: cl.BackChannelLogoutURI = "http://some-url.com/" - backChannels[id] = append(backChannels[id], *cl) + backChannels[id] = append(backChannels[id], cl) + backChannelsBySubject[subject] = append(backChannelsBySubject[subject], cl) case 2: cl.FrontChannelLogoutURI = "http://some-url.com/" cl.BackChannelLogoutURI = "http://some-url.com/" - frontChannels[id] = append(frontChannels[id], *cl) - backChannels[id] = append(backChannels[id], *cl) + frontChannels[id] = append(frontChannels[id], cl) + backChannels[id] = append(backChannels[id], cl) + backChannelsBySubject[subject] = append(backChannelsBySubject[subject], cl) } require.NoError(t, clientManager.CreateClient(context.Background(), cl)) @@ -736,38 +740,44 @@ func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.Fosit } for _, ls := range sessions { - check := func(t *testing.T, expected map[string][]client.Client, actual []client.Client) { - es, ok := expected[ls.ID] + check := func(t *testing.T, expected map[string][]*client.Client, expectedSetKey string, actual []client.LoginSessionClient) { + es, ok := expected[expectedSetKey] if !ok { require.Len(t, actual, 0) return } require.Len(t, actual, len(es)) + var found int for _, e := range es { - var found bool for _, a := range actual { if e.OutfacingID == a.OutfacingID { - found = true + found++ + assert.Equal(t, e.OutfacingID, a.OutfacingID) + assert.Equal(t, e.FrontChannelLogoutURI, a.FrontChannelLogoutURI) + assert.Equal(t, e.BackChannelLogoutURI, a.BackChannelLogoutURI) } - assert.Equal(t, e.OutfacingID, a.OutfacingID) - assert.Equal(t, e.FrontChannelLogoutURI, a.FrontChannelLogoutURI) - assert.Equal(t, e.BackChannelLogoutURI, a.BackChannelLogoutURI) } - require.True(t, found) } + require.Len(t, es, found) } - t.Run(fmt.Sprintf("method=ListUserAuthenticatedClientsWithFrontChannelLogout/session=%s/subject=%s", ls.ID, ls.Subject), func(t *testing.T) { - actual, err := m.ListUserAuthenticatedClientsWithFrontChannelLogout(context.Background(), ls.Subject, ls.ID) + t.Run(fmt.Sprintf("method=ListUserSessionAuthenticatedClientsWithFrontChannelLogout/session=%s/subject=%s", ls.ID, ls.Subject), func(t *testing.T) { + actual, err := m.ListUserSessionAuthenticatedClientsWithFrontChannelLogout(context.Background(), ls.Subject, ls.ID) + require.NoError(t, err) + check(t, frontChannels, ls.ID, actual) + }) + + t.Run(fmt.Sprintf("method=ListUserSessionAuthenticatedClientsWithBackChannelLogout/session=%s", ls.ID), func(t *testing.T) { + actual, err := m.ListUserSessionAuthenticatedClientsWithBackChannelLogout(context.Background(), ls.Subject, ls.ID) require.NoError(t, err) - check(t, frontChannels, actual) + check(t, backChannels, ls.ID, actual) }) - t.Run(fmt.Sprintf("method=ListUserAuthenticatedClientsWithBackChannelLogout/session=%s", ls.ID), func(t *testing.T) { - actual, err := m.ListUserAuthenticatedClientsWithBackChannelLogout(context.Background(), ls.Subject, ls.ID) + t.Run(fmt.Sprintf("method=ListUserAuthenticatedClientsWithBackChannelLogout/subject=%s", ls.Subject), func(t *testing.T) { + actual, err := m.ListUserAuthenticatedClientsWithBackChannelLogout(context.Background(), ls.Subject) require.NoError(t, err) - check(t, backChannels, actual) + check(t, backChannelsBySubject, ls.Subject, actual) }) } }) diff --git a/consent/strategy.go b/consent/strategy.go index fa2f9eebfed..7b2a6d6896e 100644 --- a/consent/strategy.go +++ b/consent/strategy.go @@ -21,6 +21,7 @@ package consent import ( + "context" "net/http" "github.com/ory/fosite" @@ -31,4 +32,8 @@ var _ Strategy = new(DefaultStrategy) type Strategy interface { HandleOAuth2AuthorizationRequest(w http.ResponseWriter, r *http.Request, req fosite.AuthorizeRequester) (*HandledConsentRequest, error) HandleOpenIDConnectLogout(w http.ResponseWriter, r *http.Request) (*LogoutResult, error) + ExecuteBackChannelLogoutBySubject(ctx context.Context, r *http.Request, subject string) + ExecuteBackChannelLogoutBySession(ctx context.Context, r *http.Request, subject, sid string) + ExecuteBackChannelLogoutByClient(ctx context.Context, r *http.Request, subject, client string) + ExecuteBackChannelLogoutByClientSession(ctx context.Context, r *http.Request, subject, client, sid string) } diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 6f67951fc05..cff1cafcef7 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -632,7 +632,7 @@ func (s *DefaultStrategy) verifyConsent(w http.ResponseWriter, r *http.Request, } func (s *DefaultStrategy) generateFrontChannelLogoutURLs(ctx context.Context, subject, sid string) ([]string, error) { - clients, err := s.r.ConsentManager().ListUserAuthenticatedClientsWithFrontChannelLogout(ctx, subject, sid) + clients, err := s.r.ConsentManager().ListUserSessionAuthenticatedClientsWithFrontChannelLogout(ctx, subject, sid) if err != nil { return nil, err } @@ -653,15 +653,57 @@ func (s *DefaultStrategy) generateFrontChannelLogoutURLs(ctx context.Context, su return urls, nil } -func (s *DefaultStrategy) executeBackChannelLogout(ctx context.Context, r *http.Request, subject, sid string) error { - clients, err := s.r.ConsentManager().ListUserAuthenticatedClientsWithBackChannelLogout(ctx, subject, sid) +func (s *DefaultStrategy) ExecuteBackChannelLogoutBySession(ctx context.Context, r *http.Request, subject, sid string) { + clients, err := s.r.ConsentManager().ListUserSessionAuthenticatedClientsWithBackChannelLogout(ctx, subject, sid) if err != nil { - return err + s.r.Logger().WithError(err).Error("Unable to execute OpenID Connect Back-Channel Logout Request") + return + } + s.executeBackChannelLogout(ctx, r, clients) +} + +func (s *DefaultStrategy) ExecuteBackChannelLogoutByClientSession(ctx context.Context, r *http.Request, subject, client, sid string) { + clients, err := s.r.ConsentManager().ListUserSessionAuthenticatedClientsWithBackChannelLogout(ctx, subject, sid) + if err != nil { + s.r.Logger().WithError(err).Error("Unable to execute OpenID Connect Back-Channel Logout Request") + return + } + for i := len(clients) - 1; i >= 0; i-- { + if clients[i].OutfacingID != client { + clients = append(clients[:i], clients[i+1:]...) + } + } + s.executeBackChannelLogout(ctx, r, clients) +} + +func (s *DefaultStrategy) ExecuteBackChannelLogoutByClient(ctx context.Context, r *http.Request, subject, client string) { + clients, err := s.r.ConsentManager().ListUserAuthenticatedClientsWithBackChannelLogout(ctx, subject) + if err != nil { + s.r.Logger().WithError(err).Error("Unable to execute OpenID Connect Back-Channel Logout Request") + return + } + for i := len(clients) - 1; i >= 0; i-- { + if clients[i].OutfacingID != client { + clients = append(clients[:i], clients[i+1:]...) + } } + s.executeBackChannelLogout(ctx, r, clients) +} +func (s *DefaultStrategy) ExecuteBackChannelLogoutBySubject(ctx context.Context, r *http.Request, subject string) { + clients, err := s.r.ConsentManager().ListUserAuthenticatedClientsWithBackChannelLogout(ctx, subject) + if err != nil { + s.r.Logger().WithError(err).Error("Unable to execute OpenID Connect Back-Channel Logout Request") + return + } + s.executeBackChannelLogout(ctx, r, clients) +} + +func (s *DefaultStrategy) executeBackChannelLogout(ctx context.Context, r *http.Request, clients []client.LoginSessionClient) { openIDKeyID, err := s.r.OpenIDJWTStrategy().GetPublicKeyID(ctx) if err != nil { - return err + s.r.Logger().WithError(err).Error("Unable to execute OpenID Connect Back-Channel Logout Request") + return } type task struct { @@ -678,19 +720,19 @@ func (s *DefaultStrategy) executeBackChannelLogout(ctx context.Context, r *http. // // s.r.ConsentManager().GetForcedObfuscatedLoginSession(context.Background(), subject, ) // sub := s.obfuscateSubjectIdentifier(c, subject, ) - t, _, err := s.r.OpenIDJWTStrategy().Generate(ctx, jwtgo.MapClaims{ "iss": s.c.IssuerURL().String(), "aud": []string{c.OutfacingID}, "iat": time.Now().UTC().Unix(), "jti": uuid.New(), "events": map[string]struct{}{"http://schemas.openid.net/event/backchannel-logout": {}}, - "sid": sid, + "sid": c.LoginSessionID, }, &jwt.Headers{ Extra: map[string]interface{}{"kid": openIDKeyID}, }) if err != nil { - return err + s.r.Logger().WithError(err).Error("Unable to execute OpenID Connect Back-Channel Logout Request") + return } tasks = append(tasks, task{url: c.BackChannelLogoutURI, clientID: c.OutfacingID, token: t}) @@ -728,8 +770,6 @@ func (s *DefaultStrategy) executeBackChannelLogout(ctx context.Context, r *http. } wg.Wait() - - return nil } func (s *DefaultStrategy) issueLogoutVerifier(w http.ResponseWriter, r *http.Request) (*LogoutResult, error) { @@ -964,9 +1004,7 @@ func (s *DefaultStrategy) completeLogout(w http.ResponseWriter, r *http.Request) return nil, err } - if err := s.executeBackChannelLogout(r.Context(), r, lr.Subject, lr.SessionID); err != nil { - return nil, err - } + s.ExecuteBackChannelLogoutBySession(r.Context(), r, lr.Subject, lr.SessionID) // TODO: Back channel execution should not interrupt logout flow? // We delete the session after back channel log out has worked as the session is otherwise removed // from the store which will break the query for finding all the channels. diff --git a/internal/httpclient-next/api/openapi.yaml b/internal/httpclient-next/api/openapi.yaml index 0f3a973cef9..6b6374e756b 100644 --- a/internal/httpclient-next/api/openapi.yaml +++ b/internal/httpclient-next/api/openapi.yaml @@ -1385,6 +1385,16 @@ paths: schema: type: string style: form + - description: If set, deletes only those consent sessions by the Subject that + have been granted to the specified session id. Can be combined with client + or all parameter. + explode: true + in: query + name: login_session_id + required: false + schema: + type: string + style: form - description: If set to `?all=true`, deletes all consent sessions by the Subject that have been granted. explode: true @@ -1394,6 +1404,15 @@ paths: schema: type: boolean style: form + - description: If set to `?trigger_back_channel_logout=true`, performs back + channel logout for matching clients + explode: true + in: query + name: trigger_back_channel_logout + required: false + schema: + type: boolean + style: form responses: "204": description: |- diff --git a/internal/httpclient-next/api_admin.go b/internal/httpclient-next/api_admin.go index 74f67b0e568..c5e7ebd12cd 100644 --- a/internal/httpclient-next/api_admin.go +++ b/internal/httpclient-next/api_admin.go @@ -4269,11 +4269,13 @@ func (a *AdminApiService) RevokeAuthenticationSessionExecute(r AdminApiApiRevoke } type AdminApiApiRevokeConsentSessionsRequest struct { - ctx context.Context - ApiService AdminApi - subject *string - client *string - all *bool + ctx context.Context + ApiService AdminApi + subject *string + client *string + loginSessionId *string + all *bool + triggerBackChannelLogout *bool } func (r AdminApiApiRevokeConsentSessionsRequest) Subject(subject string) AdminApiApiRevokeConsentSessionsRequest { @@ -4284,10 +4286,18 @@ func (r AdminApiApiRevokeConsentSessionsRequest) Client(client string) AdminApiA r.client = &client return r } +func (r AdminApiApiRevokeConsentSessionsRequest) LoginSessionId(loginSessionId string) AdminApiApiRevokeConsentSessionsRequest { + r.loginSessionId = &loginSessionId + return r +} func (r AdminApiApiRevokeConsentSessionsRequest) All(all bool) AdminApiApiRevokeConsentSessionsRequest { r.all = &all return r } +func (r AdminApiApiRevokeConsentSessionsRequest) TriggerBackChannelLogout(triggerBackChannelLogout bool) AdminApiApiRevokeConsentSessionsRequest { + r.triggerBackChannelLogout = &triggerBackChannelLogout + return r +} func (r AdminApiApiRevokeConsentSessionsRequest) Execute() (*http.Response, error) { return r.ApiService.RevokeConsentSessionsExecute(r) @@ -4337,9 +4347,15 @@ func (a *AdminApiService) RevokeConsentSessionsExecute(r AdminApiApiRevokeConsen if r.client != nil { localVarQueryParams.Add("client", parameterToString(*r.client, "")) } + if r.loginSessionId != nil { + localVarQueryParams.Add("login_session_id", parameterToString(*r.loginSessionId, "")) + } if r.all != nil { localVarQueryParams.Add("all", parameterToString(*r.all, "")) } + if r.triggerBackChannelLogout != nil { + localVarQueryParams.Add("trigger_back_channel_logout", parameterToString(*r.triggerBackChannelLogout, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient-next/docs/AdminApi.md b/internal/httpclient-next/docs/AdminApi.md index d4df48e60fa..37585e79a8e 100644 --- a/internal/httpclient-next/docs/AdminApi.md +++ b/internal/httpclient-next/docs/AdminApi.md @@ -1879,7 +1879,7 @@ No authorization required ## RevokeConsentSessions -> RevokeConsentSessions(ctx).Subject(subject).Client(client).All(all).Execute() +> RevokeConsentSessions(ctx).Subject(subject).Client(client).LoginSessionId(loginSessionId).All(all).TriggerBackChannelLogout(triggerBackChannelLogout).Execute() Revokes Consent Sessions of a Subject for a Specific OAuth 2.0 Client @@ -1900,11 +1900,13 @@ import ( func main() { subject := "subject_example" // string | The subject (Subject) who's consent sessions should be deleted. client := "client_example" // string | If set, deletes only those consent sessions by the Subject that have been granted to the specified OAuth 2.0 Client ID (optional) + loginSessionId := "loginSessionId_example" // string | If set, deletes only those consent sessions by the Subject that have been granted to the specified session id. Can be combined with client or all parameter. (optional) all := true // bool | If set to `?all=true`, deletes all consent sessions by the Subject that have been granted. (optional) + triggerBackChannelLogout := true // bool | If set to `?trigger_back_channel_logout=true`, performs back channel logout for matching clients (optional) configuration := openapiclient.NewConfiguration() apiClient := openapiclient.NewAPIClient(configuration) - resp, r, err := apiClient.AdminApi.RevokeConsentSessions(context.Background()).Subject(subject).Client(client).All(all).Execute() + resp, r, err := apiClient.AdminApi.RevokeConsentSessions(context.Background()).Subject(subject).Client(client).LoginSessionId(loginSessionId).All(all).TriggerBackChannelLogout(triggerBackChannelLogout).Execute() if err != nil { fmt.Fprintf(os.Stderr, "Error when calling `AdminApi.RevokeConsentSessions``: %v\n", err) fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) @@ -1925,7 +1927,9 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **subject** | **string** | The subject (Subject) who's consent sessions should be deleted. | **client** | **string** | If set, deletes only those consent sessions by the Subject that have been granted to the specified OAuth 2.0 Client ID | + **loginSessionId** | **string** | If set, deletes only those consent sessions by the Subject that have been granted to the specified session id. Can be combined with client or all parameter. | **all** | **bool** | If set to `?all=true`, deletes all consent sessions by the Subject that have been granted. | + **triggerBackChannelLogout** | **bool** | If set to `?trigger_back_channel_logout=true`, performs back channel logout for matching clients | ### Return type diff --git a/internal/httpclient/client/admin/revoke_consent_sessions_parameters.go b/internal/httpclient/client/admin/revoke_consent_sessions_parameters.go index 357224b576f..109f2322d6b 100644 --- a/internal/httpclient/client/admin/revoke_consent_sessions_parameters.go +++ b/internal/httpclient/client/admin/revoke_consent_sessions_parameters.go @@ -72,12 +72,24 @@ type RevokeConsentSessionsParams struct { */ Client *string + /* LoginSessionID. + + If set, deletes only those consent sessions by the Subject that have been granted to the specified session id. Can be combined with client or all parameter. + */ + LoginSessionID *string + /* Subject. The subject (Subject) who's consent sessions should be deleted. */ Subject string + /* TriggerBackChannelLogout. + + If set to `?trigger_back_channel_logout=true`, performs back channel logout for matching clients + */ + TriggerBackChannelLogout *bool + timeout time.Duration Context context.Context HTTPClient *http.Client @@ -153,6 +165,17 @@ func (o *RevokeConsentSessionsParams) SetClient(client *string) { o.Client = client } +// WithLoginSessionID adds the loginSessionID to the revoke consent sessions params +func (o *RevokeConsentSessionsParams) WithLoginSessionID(loginSessionID *string) *RevokeConsentSessionsParams { + o.SetLoginSessionID(loginSessionID) + return o +} + +// SetLoginSessionID adds the loginSessionId to the revoke consent sessions params +func (o *RevokeConsentSessionsParams) SetLoginSessionID(loginSessionID *string) { + o.LoginSessionID = loginSessionID +} + // WithSubject adds the subject to the revoke consent sessions params func (o *RevokeConsentSessionsParams) WithSubject(subject string) *RevokeConsentSessionsParams { o.SetSubject(subject) @@ -164,6 +187,17 @@ func (o *RevokeConsentSessionsParams) SetSubject(subject string) { o.Subject = subject } +// WithTriggerBackChannelLogout adds the triggerBackChannelLogout to the revoke consent sessions params +func (o *RevokeConsentSessionsParams) WithTriggerBackChannelLogout(triggerBackChannelLogout *bool) *RevokeConsentSessionsParams { + o.SetTriggerBackChannelLogout(triggerBackChannelLogout) + return o +} + +// SetTriggerBackChannelLogout adds the triggerBackChannelLogout to the revoke consent sessions params +func (o *RevokeConsentSessionsParams) SetTriggerBackChannelLogout(triggerBackChannelLogout *bool) { + o.TriggerBackChannelLogout = triggerBackChannelLogout +} + // WriteToRequest writes these params to a swagger request func (o *RevokeConsentSessionsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { @@ -206,6 +240,23 @@ func (o *RevokeConsentSessionsParams) WriteToRequest(r runtime.ClientRequest, re } } + if o.LoginSessionID != nil { + + // query param login_session_id + var qrLoginSessionID string + + if o.LoginSessionID != nil { + qrLoginSessionID = *o.LoginSessionID + } + qLoginSessionID := qrLoginSessionID + if qLoginSessionID != "" { + + if err := r.SetQueryParam("login_session_id", qLoginSessionID); err != nil { + return err + } + } + } + // query param subject qrSubject := o.Subject qSubject := qrSubject @@ -216,6 +267,23 @@ func (o *RevokeConsentSessionsParams) WriteToRequest(r runtime.ClientRequest, re } } + if o.TriggerBackChannelLogout != nil { + + // query param trigger_back_channel_logout + var qrTriggerBackChannelLogout bool + + if o.TriggerBackChannelLogout != nil { + qrTriggerBackChannelLogout = *o.TriggerBackChannelLogout + } + qTriggerBackChannelLogout := swag.FormatBool(qrTriggerBackChannelLogout) + if qTriggerBackChannelLogout != "" { + + if err := r.SetQueryParam("trigger_back_channel_logout", qTriggerBackChannelLogout); err != nil { + return err + } + } + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } diff --git a/oauth2/oauth2_helper_test.go b/oauth2/oauth2_helper_test.go index 4fb665eabbf..c982594fcd0 100644 --- a/oauth2/oauth2_helper_test.go +++ b/oauth2/oauth2_helper_test.go @@ -21,6 +21,7 @@ package oauth2_test import ( + "context" "net/http" "time" @@ -62,3 +63,19 @@ func (c *consentMock) HandleOAuth2AuthorizationRequest(w http.ResponseWriter, r func (c *consentMock) HandleOpenIDConnectLogout(w http.ResponseWriter, r *http.Request) (*consent.LogoutResult, error) { panic("not implemented") } + +func (c *consentMock) ExecuteBackChannelLogoutBySession(ctx context.Context, r *http.Request, subject, sid string) { + panic("not implemented") +} + +func (c *consentMock) ExecuteBackChannelLogoutByClientSession(ctx context.Context, r *http.Request, subject, client, sid string) { + panic("not implemented") +} + +func (c *consentMock) ExecuteBackChannelLogoutByClient(ctx context.Context, r *http.Request, subject, client string) { + panic("not implemented") +} + +func (c *consentMock) ExecuteBackChannelLogoutBySubject(ctx context.Context, r *http.Request, subject string) { + panic("not implemented") +} diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 923e951fa4a..4d20d654d3f 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -6,6 +6,8 @@ import ( "fmt" "time" + "github.com/ory/hydra/client" + "github.com/gobuffalo/pop/v6" "github.com/ory/x/sqlxx" @@ -15,7 +17,6 @@ import ( "github.com/pkg/errors" "github.com/ory/fosite" - "github.com/ory/hydra/client" "github.com/ory/hydra/consent" "github.com/ory/hydra/x" "github.com/ory/x/sqlcon" @@ -27,10 +28,18 @@ func (p *Persister) RevokeSubjectConsentSession(ctx context.Context, user string return p.transaction(ctx, p.revokeConsentSession("r.subject = ?", user)) } +func (p *Persister) RevokeLoginSessionConsentSession(ctx context.Context, loginSessionId string) error { + return p.transaction(ctx, p.revokeConsentSession("r.login_session_id = ?", loginSessionId)) +} + func (p *Persister) RevokeSubjectClientConsentSession(ctx context.Context, user, client string) error { return p.transaction(ctx, p.revokeConsentSession("r.subject = ? AND r.client_id = ?", user, client)) } +func (p *Persister) RevokeSubjectClientLoginSessionConsentSession(ctx context.Context, user, client, loginSessionId string) error { + return p.transaction(ctx, p.revokeConsentSession("r.subject = ? AND r.client_id = ? AND r.login_session_id = ?", user, client, loginSessionId)) +} + func (p *Persister) revokeConsentSession(whereStmt string, whereArgs ...interface{}) func(context.Context, *pop.Connection) error { return func(ctx context.Context, c *pop.Connection) error { hrs := make([]*consent.HandledConsentRequest, 0) @@ -364,20 +373,24 @@ func (p *Persister) resolveHandledConsentRequests(ctx context.Context, requests return result, nil } -func (p *Persister) ListUserAuthenticatedClientsWithFrontChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) { - return p.listUserAuthenticatedClients(ctx, subject, sid, "front") +func (p *Persister) ListUserSessionAuthenticatedClientsWithFrontChannelLogout(ctx context.Context, subject, sid string) ([]client.LoginSessionClient, error) { + return p.listUserSessionAuthenticatedClients(ctx, subject, sid, "front") } -func (p *Persister) ListUserAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) { - return p.listUserAuthenticatedClients(ctx, subject, sid, "back") +func (p *Persister) ListUserSessionAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject, sid string) ([]client.LoginSessionClient, error) { + return p.listUserSessionAuthenticatedClients(ctx, subject, sid, "back") } -func (p *Persister) listUserAuthenticatedClients(ctx context.Context, subject, sid, channel string) ([]client.Client, error) { - var cs []client.Client - return cs, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { +func (p *Persister) ListUserAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject string) ([]client.LoginSessionClient, error) { + return p.listUserAuthenticatedClients(ctx, subject, "back") +} + +func (p *Persister) listUserSessionAuthenticatedClients(ctx context.Context, subject, sid, channel string) ([]client.LoginSessionClient, error) { + var cs []client.LoginSessionClient + err := p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { if err := c.RawQuery( /* #nosec G201 - channel can either be "front" or "back" */ - fmt.Sprintf(`SELECT DISTINCT c.* FROM hydra_client as c JOIN hydra_oauth2_consent_request as r ON (c.id = r.client_id) WHERE r.subject=? AND c.%schannel_logout_uri!='' AND c.%schannel_logout_uri IS NOT NULL AND r.login_session_id = ?`, + fmt.Sprintf(`SELECT DISTINCT c.id, c.frontchannel_logout_uri, c.frontchannel_logout_session_required, c.backchannel_logout_uri, c.backchannel_logout_session_required, r.login_session_id FROM hydra_client as c JOIN hydra_oauth2_consent_request as r ON (c.id = r.client_id) WHERE r.subject=? AND c.%schannel_logout_uri!='' AND c.%schannel_logout_uri IS NOT NULL AND r.login_session_id = ?`, channel, channel, ), @@ -387,6 +400,28 @@ func (p *Persister) listUserAuthenticatedClients(ctx context.Context, subject, s return sqlcon.HandleError(err) } + return nil + }) + if err != nil { + return nil, err + } + return cs, err +} + +func (p *Persister) listUserAuthenticatedClients(ctx context.Context, subject, channel string) ([]client.LoginSessionClient, error) { + var cs []client.LoginSessionClient + return cs, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := c.RawQuery( + /* #nosec G201 - channel can either be "front" or "back" */ + fmt.Sprintf(`SELECT DISTINCT c.id, c.frontchannel_logout_uri, c.frontchannel_logout_session_required, c.backchannel_logout_uri, c.backchannel_logout_session_required, r.login_session_id FROM hydra_client as c JOIN hydra_oauth2_consent_request as r ON (c.id = r.client_id) WHERE r.subject=? AND c.%schannel_logout_uri!='' AND c.%schannel_logout_uri IS NOT NULL`, + channel, + channel, + ), + subject, + ).All(&cs); err != nil { + return sqlcon.HandleError(err) + } + return nil }) } diff --git a/spec/api.json b/spec/api.json index b18c236a5dc..4d3e34fe3ff 100755 --- a/spec/api.json +++ b/spec/api.json @@ -3097,6 +3097,14 @@ "type": "string" } }, + { + "description": "If set, deletes only those consent sessions by the Subject that have been granted to the specified session id. Can be combined with client or all parameter.", + "in": "query", + "name": "login_session_id", + "schema": { + "type": "string" + } + }, { "description": "If set to `?all=true`, deletes all consent sessions by the Subject that have been granted.", "in": "query", @@ -3104,6 +3112,14 @@ "schema": { "type": "boolean" } + }, + { + "description": "If set to `?trigger_back_channel_logout=true`, performs back channel logout for matching clients", + "in": "query", + "name": "trigger_back_channel_logout", + "schema": { + "type": "boolean" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index 38e4461fe4d..bb3fe1366c4 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1611,11 +1611,23 @@ "name": "client", "in": "query" }, + { + "type": "string", + "description": "If set, deletes only those consent sessions by the Subject that have been granted to the specified session id. Can be combined with client or all parameter.", + "name": "login_session_id", + "in": "query" + }, { "type": "boolean", "description": "If set to `?all=true`, deletes all consent sessions by the Subject that have been granted.", "name": "all", "in": "query" + }, + { + "type": "boolean", + "description": "If set to `?trigger_back_channel_logout=true`, performs back channel logout for matching clients", + "name": "trigger_back_channel_logout", + "in": "query" } ], "responses": {