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 Mar 30, 2022
1 parent 00100a1 commit 3946f69
Show file tree
Hide file tree
Showing 16 changed files with 614 additions and 59 deletions.
5 changes: 5 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
10 changes: 10 additions & 0 deletions consent/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 35 additions & 6 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.`)))
Expand All @@ -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.`)))
Expand Down
268 changes: 268 additions & 0 deletions consent/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
}
})
}
7 changes: 5 additions & 2 deletions consent/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 3946f69

Please sign in to comment.