Skip to content

Commit

Permalink
feat: revoke consent by session id. trigger back channel logout
Browse files Browse the repository at this point in the history
  • Loading branch information
aarmam committed Dec 25, 2022
1 parent bfb7d62 commit 6a88521
Show file tree
Hide file tree
Showing 15 changed files with 1,279 additions and 31 deletions.
5 changes: 5 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,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
274 changes: 274 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/driver"

"github.com/ory/x/pointerx"

"github.com/ory/hydra/consent"
Expand Down Expand Up @@ -277,3 +284,270 @@ 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

0 comments on commit 6a88521

Please sign in to comment.