Skip to content

Commit f5cd46d

Browse files
committed
feat: revoke consent by session id. trigger back channel logout
1 parent 3b1d87e commit f5cd46d

15 files changed

+1278
-31
lines changed

client/client.go

+5
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,11 @@ type Client struct {
308308
Lifespans
309309
}
310310

311+
type LoginSessionClient struct {
312+
Client
313+
LoginSessionID string `json:"login_session_id,omitempty" db:"login_session_id"`
314+
}
315+
311316
// OAuth 2.0 Client Token Lifespans
312317
//
313318
// Lifespans of different token types issued for this OAuth 2.0 Client.

consent/handler.go

+46-6
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,22 @@ type revokeOAuth2ConsentSessions struct {
8484
// in: query
8585
Client string `json:"client"`
8686

87+
// 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.
88+
//
89+
// in: query
90+
LoginSessionId string `json:"login_session_id"`
91+
8792
// Revoke All Consent Sessions
8893
//
8994
// If set to `true` deletes all consent sessions by the Subject that have been granted.
9095
//
9196
// in: query
9297
All bool `json:"all"`
98+
99+
// If set to `?trigger_back_channel_logout=true`, performs back channel logout for matching clients
100+
//
101+
// in: query
102+
TriggerBackChannelLogout bool `json:"trigger_back_channel_logout"`
93103
}
94104

95105
// swagger:route DELETE /admin/oauth2/auth/sessions/consent oAuth2 revokeOAuth2ConsentSessions
@@ -113,22 +123,52 @@ type revokeOAuth2ConsentSessions struct {
113123
func (h *Handler) revokeOAuth2ConsentSessions(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
114124
subject := r.URL.Query().Get("subject")
115125
client := r.URL.Query().Get("client")
126+
loginSessionId := r.URL.Query().Get("login_session_id")
127+
triggerBackChannelLogout := r.URL.Query().Get("trigger_back_channel_logout")
116128
allClients := r.URL.Query().Get("all") == "true"
129+
117130
if subject == "" {
118131
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'subject' is not defined but should have been.`)))
119132
return
120133
}
121134

122135
switch {
123136
case len(client) > 0:
124-
if err := h.r.ConsentManager().RevokeSubjectClientConsentSession(r.Context(), subject, client); err != nil && !errors.Is(err, x.ErrNotFound) {
125-
h.r.Writer().WriteError(w, r, err)
126-
return
137+
if len(loginSessionId) > 0 {
138+
if triggerBackChannelLogout == "true" {
139+
h.r.ConsentStrategy().ExecuteBackChannelLogoutByClientSession(r.Context(), r, subject, client, loginSessionId)
140+
}
141+
if err := h.r.ConsentManager().RevokeSubjectClientLoginSessionConsentSession(r.Context(), subject, client, loginSessionId); err != nil && !errors.Is(err, x.ErrNotFound) {
142+
h.r.Writer().WriteError(w, r, err)
143+
return
144+
}
145+
} else {
146+
if triggerBackChannelLogout == "true" {
147+
h.r.ConsentStrategy().ExecuteBackChannelLogoutByClient(r.Context(), r, subject, client)
148+
}
149+
if err := h.r.ConsentManager().RevokeSubjectClientConsentSession(r.Context(), subject, client); err != nil && !errors.Is(err, x.ErrNotFound) {
150+
h.r.Writer().WriteError(w, r, err)
151+
return
152+
}
127153
}
154+
128155
case allClients:
129-
if err := h.r.ConsentManager().RevokeSubjectConsentSession(r.Context(), subject); err != nil && !errors.Is(err, x.ErrNotFound) {
130-
h.r.Writer().WriteError(w, r, err)
131-
return
156+
if len(loginSessionId) > 0 {
157+
if triggerBackChannelLogout == "true" {
158+
h.r.ConsentStrategy().ExecuteBackChannelLogoutBySession(r.Context(), r, subject, loginSessionId)
159+
}
160+
if err := h.r.ConsentManager().RevokeLoginSessionConsentSession(r.Context(), loginSessionId); err != nil && !errors.Is(err, x.ErrNotFound) {
161+
h.r.Writer().WriteError(w, r, err)
162+
return
163+
}
164+
} else {
165+
if triggerBackChannelLogout == "true" {
166+
h.r.ConsentStrategy().ExecuteBackChannelLogoutBySubject(r.Context(), r, subject)
167+
}
168+
if err := h.r.ConsentManager().RevokeSubjectConsentSession(r.Context(), subject); err != nil && !errors.Is(err, x.ErrNotFound) {
169+
h.r.Writer().WriteError(w, r, err)
170+
return
171+
}
132172
}
133173
default:
134174
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.`)))

consent/handler_test.go

+273
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ import (
1010
"fmt"
1111
"net/http"
1212
"net/http/httptest"
13+
"net/url"
14+
"sync"
1315
"testing"
1416
"time"
1517

18+
"github.com/stretchr/testify/assert"
19+
"github.com/tidwall/gjson"
20+
21+
"github.com/ory/hydra/driver"
22+
1623
"github.com/ory/x/pointerx"
1724

1825
"github.com/ory/hydra/v2/x"
@@ -276,3 +283,269 @@ func TestGetLoginRequestWithDuplicateAccept(t *testing.T) {
276283
require.Contains(t, result2.RedirectTo, "login_verifier")
277284
})
278285
}
286+
287+
func TestRevokeConsentSession(t *testing.T) {
288+
newWg := func(add int) *sync.WaitGroup {
289+
var wg sync.WaitGroup
290+
wg.Add(add)
291+
return &wg
292+
}
293+
294+
t.Run("case=subject=subject-1,client=client-1,session=session-1,trigger_back_channel_logout=true", func(t *testing.T) {
295+
conf := internal.NewConfigurationWithDefaults()
296+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
297+
backChannelWG := newWg(1)
298+
cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1"}, backChannelWG)
299+
performLoginFlow(t, reg, "1", cl)
300+
performLoginFlow(t, reg, "2", cl)
301+
performDeleteConsentSession(t, reg, "client-1", "login-session-1", true)
302+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
303+
require.Error(t, x.ErrNotFound, err)
304+
require.Nil(t, c1)
305+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
306+
require.NoError(t, err)
307+
require.NotNil(t, c2)
308+
backChannelWG.Wait()
309+
})
310+
311+
t.Run("case=subject=subject-1,client=client-1,session=session-1,trigger_back_channel_logout=false", func(t *testing.T) {
312+
conf := internal.NewConfigurationWithDefaults()
313+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
314+
backChannelWG := newWg(0)
315+
cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG)
316+
performLoginFlow(t, reg, "1", cl)
317+
performLoginFlow(t, reg, "2", cl)
318+
performDeleteConsentSession(t, reg, "client-1", "login-session-1", false)
319+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
320+
require.Error(t, x.ErrNotFound, err)
321+
require.Nil(t, c1)
322+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
323+
require.NoError(t, err)
324+
require.NotNil(t, c2)
325+
backChannelWG.Wait()
326+
})
327+
328+
t.Run("case=subject=subject-1,client=client-1,trigger_back_channel_logout=true", func(t *testing.T) {
329+
conf := internal.NewConfigurationWithDefaults()
330+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
331+
backChannelWG := newWg(2)
332+
cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1", "login-session-2"}, backChannelWG)
333+
performLoginFlow(t, reg, "1", cl)
334+
performLoginFlow(t, reg, "2", cl)
335+
336+
performDeleteConsentSession(t, reg, "client-1", nil, true)
337+
338+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
339+
require.Error(t, x.ErrNotFound, err)
340+
require.Nil(t, c1)
341+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
342+
require.Error(t, x.ErrNotFound, err)
343+
require.Nil(t, c2)
344+
backChannelWG.Wait()
345+
})
346+
347+
t.Run("case=subject=subject-1,client=client-1,trigger_back_channel_logout=false", func(t *testing.T) {
348+
conf := internal.NewConfigurationWithDefaults()
349+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
350+
backChannelWG := newWg(0)
351+
cl := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG)
352+
performLoginFlow(t, reg, "1", cl)
353+
performLoginFlow(t, reg, "2", cl)
354+
355+
performDeleteConsentSession(t, reg, "client-1", nil, false)
356+
357+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
358+
require.Error(t, x.ErrNotFound, err)
359+
require.Nil(t, c1)
360+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
361+
require.Error(t, x.ErrNotFound, err)
362+
require.Nil(t, c2)
363+
backChannelWG.Wait()
364+
})
365+
366+
t.Run("case=subject=subject-1,all=true,session=session-1,trigger_back_channel_logout=true", func(t *testing.T) {
367+
conf := internal.NewConfigurationWithDefaults()
368+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
369+
backChannelWG := newWg(1)
370+
cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1"}, backChannelWG)
371+
cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{}, backChannelWG)
372+
performLoginFlow(t, reg, "1", cl1)
373+
performLoginFlow(t, reg, "2", cl2)
374+
375+
performDeleteConsentSession(t, reg, nil, "login-session-1", true)
376+
377+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
378+
require.Error(t, x.ErrNotFound, err)
379+
require.Nil(t, c1)
380+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
381+
require.NoError(t, err)
382+
require.NotNil(t, c2)
383+
backChannelWG.Wait()
384+
})
385+
386+
t.Run("case=subject=subject-1,all=true,session=session-1,trigger_back_channel_logout=false", func(t *testing.T) {
387+
conf := internal.NewConfigurationWithDefaults()
388+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
389+
backChannelWG := newWg(0)
390+
cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG)
391+
cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{}, backChannelWG)
392+
performLoginFlow(t, reg, "1", cl1)
393+
performLoginFlow(t, reg, "2", cl2)
394+
395+
performDeleteConsentSession(t, reg, nil, "login-session-1", false)
396+
397+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
398+
require.Error(t, x.ErrNotFound, err)
399+
require.Nil(t, c1)
400+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
401+
require.NoError(t, err)
402+
require.NotNil(t, c2)
403+
backChannelWG.Wait()
404+
})
405+
406+
t.Run("case=subject=subject-1,all=true,trigger_back_channel_logout=true", func(t *testing.T) {
407+
conf := internal.NewConfigurationWithDefaults()
408+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
409+
backChannelWG := newWg(2)
410+
cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{"login-session-1"}, backChannelWG)
411+
cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{"login-session-2"}, backChannelWG)
412+
performLoginFlow(t, reg, "1", cl1)
413+
performLoginFlow(t, reg, "2", cl2)
414+
415+
performDeleteConsentSession(t, reg, nil, nil, true)
416+
417+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
418+
require.Error(t, x.ErrNotFound, err)
419+
require.Nil(t, c1)
420+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
421+
require.Error(t, x.ErrNotFound, err)
422+
require.Nil(t, c2)
423+
backChannelWG.Wait()
424+
})
425+
426+
t.Run("case=subject=subject-1,all=true,trigger_back_channel_logout=false", func(t *testing.T) {
427+
conf := internal.NewConfigurationWithDefaults()
428+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
429+
backChannelWG := newWg(0)
430+
cl1 := createClientWithBackChannelEndpoint(t, reg, "client-1", []string{}, backChannelWG)
431+
cl2 := createClientWithBackChannelEndpoint(t, reg, "client-2", []string{}, backChannelWG)
432+
performLoginFlow(t, reg, "1", cl1)
433+
performLoginFlow(t, reg, "2", cl2)
434+
435+
performDeleteConsentSession(t, reg, nil, nil, false)
436+
437+
c1, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-1")
438+
require.Error(t, x.ErrNotFound, err)
439+
require.Nil(t, c1)
440+
c2, err := reg.ConsentManager().GetConsentRequest(context.Background(), "consent-challenge-2")
441+
require.Error(t, x.ErrNotFound, err)
442+
require.Nil(t, c2)
443+
backChannelWG.Wait()
444+
})
445+
}
446+
447+
func performDeleteConsentSession(t *testing.T, reg driver.Registry, client, loginSessionId interface{}, triggerBackChannelLogout bool) {
448+
conf := internal.NewConfigurationWithDefaults()
449+
h := NewHandler(reg, conf)
450+
r := x.NewRouterAdmin(conf.AdminURL)
451+
h.SetRoutes(r)
452+
ts := httptest.NewServer(r)
453+
defer ts.Close()
454+
c := &http.Client{}
455+
456+
u, _ := url.Parse(ts.URL + "/admin" + SessionsPath + "/consent")
457+
q := u.Query()
458+
q.Set("subject", "subject-1")
459+
if client != nil && len(client.(string)) != 0 {
460+
q.Set("client", client.(string))
461+
} else {
462+
q.Set("all", "true")
463+
}
464+
if loginSessionId != nil && len(loginSessionId.(string)) != 0 {
465+
q.Set("login_session_id", loginSessionId.(string))
466+
}
467+
if triggerBackChannelLogout {
468+
q.Set("trigger_back_channel_logout", "true")
469+
}
470+
u.RawQuery = q.Encode()
471+
req, err := http.NewRequest(http.MethodDelete, u.String(), nil)
472+
473+
require.NoError(t, err)
474+
_, err = c.Do(req)
475+
require.NoError(t, err)
476+
}
477+
478+
func performLoginFlow(t *testing.T, reg driver.Registry, flowId string, cl *client.Client) {
479+
subject := "subject-1"
480+
loginSessionId := "login-session-" + flowId
481+
loginChallenge := "login-challenge-" + flowId
482+
consentChallenge := "consent-challenge-" + flowId
483+
requestURL := "http://192.0.2.1"
484+
485+
ls := &LoginSession{
486+
ID: loginSessionId,
487+
Subject: subject,
488+
}
489+
lr := &LoginRequest{
490+
ID: loginChallenge,
491+
Subject: subject,
492+
Client: cl,
493+
RequestURL: requestURL,
494+
Verifier: "login-verifier-" + flowId,
495+
SessionID: sqlxx.NullString(loginSessionId),
496+
}
497+
cr := &OAuth2ConsentRequest{
498+
Client: cl,
499+
ID: consentChallenge,
500+
Verifier: consentChallenge,
501+
CSRF: consentChallenge,
502+
Subject: subject,
503+
LoginChallenge: sqlxx.NullString(loginChallenge),
504+
LoginSessionID: sqlxx.NullString(loginSessionId),
505+
}
506+
hcr := &AcceptOAuth2ConsentRequest{
507+
ConsentRequest: cr,
508+
ID: consentChallenge,
509+
WasHandled: true,
510+
HandledAt: sqlxx.NullTime(time.Now().UTC()),
511+
}
512+
513+
require.NoError(t, reg.ConsentManager().CreateLoginSession(context.Background(), ls))
514+
require.NoError(t, reg.ConsentManager().CreateLoginRequest(context.Background(), lr))
515+
require.NoError(t, reg.ConsentManager().CreateConsentRequest(context.Background(), cr))
516+
_, err := reg.ConsentManager().HandleConsentRequest(context.Background(), hcr)
517+
require.NoError(t, err)
518+
}
519+
520+
func createClientWithBackChannelEndpoint(t *testing.T, reg driver.Registry, clientId string, expectedBackChannelLogoutFlowIds []string, wg *sync.WaitGroup) *client.Client {
521+
return func(t *testing.T, key string, wg *sync.WaitGroup, cb func(t *testing.T, logoutToken gjson.Result)) *client.Client {
522+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
523+
defer wg.Done()
524+
require.NoError(t, r.ParseForm())
525+
lt := r.PostFormValue("logout_token")
526+
assert.NotEmpty(t, lt)
527+
token, err := reg.OpenIDJWTStrategy().Decode(r.Context(), lt)
528+
require.NoError(t, err)
529+
var b bytes.Buffer
530+
require.NoError(t, json.NewEncoder(&b).Encode(token.Claims))
531+
cb(t, gjson.Parse(b.String()))
532+
}))
533+
t.Cleanup(server.Close)
534+
c := &client.Client{
535+
LegacyClientID: clientId,
536+
BackChannelLogoutURI: server.URL,
537+
}
538+
err := reg.ClientManager().CreateClient(context.Background(), c)
539+
require.NoError(t, err)
540+
return c
541+
}(t, clientId, wg, func(t *testing.T, logoutToken gjson.Result) {
542+
sid := logoutToken.Get("sid").String()
543+
assert.Contains(t, expectedBackChannelLogoutFlowIds, sid)
544+
for i, v := range expectedBackChannelLogoutFlowIds {
545+
if v == sid {
546+
expectedBackChannelLogoutFlowIds = append(expectedBackChannelLogoutFlowIds[:i], expectedBackChannelLogoutFlowIds[i+1:]...)
547+
break
548+
}
549+
}
550+
})
551+
}

0 commit comments

Comments
 (0)