Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: revoke consent by session id. trigger back channel logout. #2844

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ type Client struct {
Lifespans
}

type LoginSessionClient struct {
Client
LoginSessionID string `json:"login_session_id,omitempty" db:"login_session_id"`
}

// OAuth 2.0 Client Token Lifespans
//
// Lifespans of different token types issued for this OAuth 2.0 Client.
Expand Down
52 changes: 46 additions & 6 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,22 @@ type revokeOAuth2ConsentSessions 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"`

// Revoke All Consent Sessions
//
// If set to `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:route DELETE /admin/oauth2/auth/sessions/consent oAuth2 revokeOAuth2ConsentSessions
Expand All @@ -113,22 +123,52 @@ type revokeOAuth2ConsentSessions struct {
func (h *Handler) revokeOAuth2ConsentSessions(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.`)))
return
}

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.`)))
Expand Down
273 changes: 273 additions & 0 deletions consent/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"

"github.com/ory/hydra/v2/driver"

"github.com/ory/x/pointerx"

"github.com/ory/hydra/v2/x"
Expand Down Expand Up @@ -276,3 +283,269 @@ func TestGetLoginRequestWithDuplicateAccept(t *testing.T) {
require.Contains(t, result2.RedirectTo, "login_verifier")
})
}

func TestRevokeConsentSession(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, &contextx.Default{})
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, &contextx.Default{})
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, &contextx.Default{})
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, &contextx.Default{})
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, &contextx.Default{})
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, &contextx.Default{})
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, &contextx.Default{})
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, &contextx.Default{})
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) {
conf := internal.NewConfigurationWithDefaults()
h := NewHandler(reg, conf)
r := x.NewRouterAdmin(conf.AdminURL)
h.SetRoutes(r)
ts := httptest.NewServer(r)
defer ts.Close()
c := &http.Client{}

u, _ := url.Parse(ts.URL + "/admin" + 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
requestURL := "http://192.0.2.1"

ls := &LoginSession{
ID: loginSessionId,
Subject: subject,
}
lr := &LoginRequest{
ID: loginChallenge,
Subject: subject,
Client: cl,
RequestURL: requestURL,
Verifier: "login-verifier-" + flowId,
SessionID: sqlxx.NullString(loginSessionId),
}
cr := &OAuth2ConsentRequest{
Client: cl,
ID: consentChallenge,
Verifier: consentChallenge,
CSRF: consentChallenge,
Subject: subject,
LoginChallenge: sqlxx.NullString(loginChallenge),
LoginSessionID: sqlxx.NullString(loginSessionId),
}
hcr := &AcceptOAuth2ConsentRequest{
ConsentRequest: cr,
ID: consentChallenge,
WasHandled: true,
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(), 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{
LegacyClientID: 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
}
}
})
}
Loading