Skip to content

Commit

Permalink
feat: extend latest and session related consents
Browse files Browse the repository at this point in the history
  • Loading branch information
aarmam committed May 19, 2022
1 parent 9bc59be commit 3c07b1c
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 2 deletions.
7 changes: 7 additions & 0 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,13 @@ func (h *Handler) AcceptConsentRequest(w http.ResponseWriter, r *http.Request, p
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
return
} else if hr.Skip {
if p.Remember && p.RememberFor > 0 { // TODO: Consider removing 'p.RememberFor > 0' to update consent validity in both ways (limited (RememberFor > 0) -> indefinitely (RememberFor = 0) and vice versa)
err = h.r.ConsentManager().ExtendConsentRequest(r.Context(), h.r.ScopeStrategy(), hr, p.RememberFor)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
return
}
}
p.Remember = false
}

Expand Down
71 changes: 71 additions & 0 deletions consent/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
package consent_test

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/ory/hydra/x"

Expand Down Expand Up @@ -213,3 +215,72 @@ func TestGetConsentRequest(t *testing.T) {
})
}
}

func TestAcceptConsentRequest(t *testing.T) {
t.Run("case=extend consent expiry time", func(t *testing.T) {
conf := internal.NewConfigurationWithDefaults()
reg := internal.NewRegistryMemory(t, conf)
h := NewHandler(reg, conf)
r := x.NewRouterAdmin()
h.SetRoutes(r)
ts := httptest.NewServer(r)
defer ts.Close()
c := &http.Client{}
cl := &client.Client{OutfacingID: "client-1"}
require.NoError(t, reg.ClientManager().CreateClient(context.Background(), cl))

var initialRememberFor time.Duration = 300
var remainingValidTime time.Duration = 100

require.NoError(t, reg.ConsentManager().CreateLoginSession(context.Background(), &LoginSession{
ID: "session-1",
Subject: "subject-1",
}))
require.NoError(t, reg.ConsentManager().CreateConsentRequest(context.Background(), &ConsentRequest{
Subject: "subject-1",
Client: cl,
ID: "challenge-1",
LoginSessionID: "session-1",
Skip: false,
}))
requestedTimeInPast := time.Now().UTC().Add(-(initialRememberFor - remainingValidTime) * time.Second)
_, err := reg.ConsentManager().HandleConsentRequest(context.Background(), "challenge-1", &HandledConsentRequest{
ID: "challenge-1",
Remember: true,
RememberFor: int(initialRememberFor),
RequestedAt: requestedTimeInPast,
WasHandled: true,
})
require.NoError(t, err)
require.NoError(t, reg.ConsentManager().CreateConsentRequest(context.Background(), &ConsentRequest{
Subject: "subject-1",
Client: cl,
ID: "challenge-2",
LoginSessionID: "session-1",
RequestedAt: time.Now().UTC(),
Skip: true,
}))

var b bytes.Buffer
var extendRememberFor time.Duration = 300
require.NoError(t, json.NewEncoder(&b).Encode(&HandledConsentRequest{
Remember: true,
RememberFor: int(extendRememberFor),
}))

req, err := http.NewRequest("PUT", ts.URL+ConsentPath+"/accept?challenge=challenge-2", &b)

require.NoError(t, err)
resp, err := c.Do(req)
require.NoError(t, err)
require.EqualValues(t, 200, resp.StatusCode)
crs, err := reg.ConsentManager().FindSubjectsGrantedConsentRequests(context.Background(), "subject-1", 100, 0)
require.NoError(t, err)
require.NotNil(t, crs)
require.EqualValues(t, 1, len(crs))
expectedRememberFor := int(initialRememberFor + extendRememberFor - remainingValidTime)
cr := crs[0]
require.EqualValues(t, "challenge-1", cr.ID)
require.InDelta(t, expectedRememberFor, cr.RememberFor, 1)
})
}
3 changes: 3 additions & 0 deletions consent/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"context"
"time"

"github.com/ory/fosite"

"github.com/ory/hydra/client"
)

Expand All @@ -41,6 +43,7 @@ type Manager interface {
CreateConsentRequest(ctx context.Context, req *ConsentRequest) error
GetConsentRequest(ctx context.Context, challenge string) (*ConsentRequest, error)
HandleConsentRequest(ctx context.Context, challenge string, r *HandledConsentRequest) (*ConsentRequest, error)
ExtendConsentRequest(ctx context.Context, scopeStrategy fosite.ScopeStrategy, cr *ConsentRequest, extendBy int) error
RevokeSubjectConsentSession(ctx context.Context, user string) error
RevokeSubjectClientConsentSession(ctx context.Context, user, client string) error

Expand Down
161 changes: 160 additions & 1 deletion consent/manager_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func SaneMockAuthRequest(t *testing.T, m Manager, ls *LoginSession, cl *client.C
return c
}

func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.FositeStorer) func(t *testing.T) {
func ManagerTests(m Manager, clientManager client.Manager, scopeStrategy fosite.ScopeStrategy, fositeManager x.FositeStorer) func(t *testing.T) {
return func(t *testing.T) {
t.Run("case=init-fks", func(t *testing.T) {
for _, k := range []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "rv1", "rv2"} {
Expand Down Expand Up @@ -486,6 +486,165 @@ func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.Fosit
}
})

t.Run("case=extend consent request", func(t *testing.T) {
cl := &client.Client{OutfacingID: "client-1"}
_ = clientManager.CreateClient(context.Background(), cl)
consentHandler := func(subject, sessionId, challenge string, initialRememberFor, remainingValidTime time.Duration) {
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
ID: sessionId,
Subject: subject,
}))
require.NoError(t, m.CreateConsentRequest(context.Background(), &ConsentRequest{
Client: cl,
Subject: subject,
ID: challenge,
LoginSessionID: sqlxx.NullString(sessionId),
Skip: false,
RequestedAt: time.Now().UTC().Add(-initialRememberFor * time.Second),
Verifier: uuid.New().String(),
RequestedScope: []string{"scopea"},
}))
requestedTimeInPast := time.Now().UTC().Round(time.Second).Add(-(initialRememberFor - remainingValidTime) * time.Second)
_, err := m.HandleConsentRequest(context.Background(), challenge, &HandledConsentRequest{
ID: challenge,
Remember: true,
RememberFor: int(initialRememberFor),
RequestedAt: requestedTimeInPast,
WasHandled: true,
GrantedScope: []string{"scopea"},
})
require.NoError(t, err)
}

t.Run("case=extend session related and latest consent expiry times", func(t *testing.T) {
var initialRememberForSession1 time.Duration = 300
var remainingValidTimeSession1 time.Duration = 100
var initialRememberForSession2 time.Duration = 300
var remainingValidTimeSession2 time.Duration = 150
subject := uuid.New().String()
session1 := uuid.New().String()
challenge1 := uuid.New().String()
challenge2 := uuid.New().String()
challenge3 := uuid.New().String()
consentHandler(subject, session1, challenge1, initialRememberForSession1, remainingValidTimeSession1)
consentHandler(subject, uuid.New().String(), challenge2, initialRememberForSession2, remainingValidTimeSession2)
cr := &ConsentRequest{
Client: cl,
Subject: subject,
ID: challenge3,
LoginSessionID: sqlxx.NullString(session1),
RequestedAt: time.Now().UTC(),
Skip: true,
}
require.NoError(t, m.CreateConsentRequest(context.Background(), cr))
var extendRememberFor time.Duration = 300
_, err := m.HandleConsentRequest(context.Background(), challenge3, &HandledConsentRequest{
ID: challenge3,
Remember: true,
RememberFor: int(extendRememberFor),
WasHandled: true,
RequestedAt: time.Now().UTC(),
})
require.NoError(t, err)
require.NoError(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, int(extendRememberFor)))
crs, err := m.FindSubjectsGrantedConsentRequests(context.Background(), subject, 100, 0)
require.NoError(t, err)
require.EqualValues(t, 2, len(crs))

cr1 := crs[1]
require.EqualValues(t, challenge1, cr1.ID)
expectedRememberFor1 := int(initialRememberForSession1 + extendRememberFor - remainingValidTimeSession1)
require.InDelta(t, expectedRememberFor1, cr1.RememberFor, 1)

cr2 := crs[0]
require.EqualValues(t, challenge2, cr2.ID)
expectedRememberFor2 := int(initialRememberForSession2 + extendRememberFor - remainingValidTimeSession2)
require.InDelta(t, expectedRememberFor2, cr2.RememberFor, 1)
})

t.Run("case=session related consent not found", func(t *testing.T) {
cr := &ConsentRequest{
Client: cl,
Subject: "subject-1",
LoginSessionID: "session-1",
}
require.ErrorIs(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, 300), ErrNoPreviousConsentFound)
})

t.Run("case=invalid requested scope", func(t *testing.T) {
subject := uuid.New().String()
session1 := uuid.New().String()
challenge1 := uuid.New().String()
challenge2 := uuid.New().String()

consentHandler(subject, session1, challenge1, 300, 100)
cr := &ConsentRequest{
Client: cl,
Subject: subject,
ID: challenge2,
LoginSessionID: sqlxx.NullString(session1),
RequestedAt: time.Now().UTC(),
Skip: true,
RequestedScope: []string{"scopeb"},
Verifier: uuid.New().String(),
}
require.NoError(t, m.CreateConsentRequest(context.Background(), cr))
var extendRememberFor time.Duration = 300
_, err := m.HandleConsentRequest(context.Background(), challenge2, &HandledConsentRequest{
ID: challenge2,
Remember: true,
RememberFor: int(extendRememberFor),
WasHandled: true,
RequestedAt: time.Now().UTC(),
})
require.NoError(t, err)

require.NoError(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, int(extendRememberFor)))

crs, err := m.FindSubjectsGrantedConsentRequests(context.Background(), subject, 100, 0)
require.NoError(t, err)
require.EqualValues(t, 1, len(crs))
cr1 := crs[0]
require.EqualValues(t, challenge1, cr1.ID)
require.EqualValues(t, 300, cr1.RememberFor)
})

t.Run("case=initial consent request expired", func(t *testing.T) {
subject := uuid.New().String()
session1 := uuid.New().String()
challenge1 := uuid.New().String()
challenge2 := uuid.New().String()

consentHandler(subject, session1, challenge1, 300, 0)
time.Sleep(time.Second)

cr := &ConsentRequest{
Client: cl,
Subject: subject,
ID: challenge2,
LoginSessionID: sqlxx.NullString(session1),
RequestedAt: time.Now().UTC(),
Skip: true,
Verifier: uuid.New().String(),
}
require.NoError(t, m.CreateConsentRequest(context.Background(), cr))
var extendRememberFor time.Duration = 300
_, err := m.HandleConsentRequest(context.Background(), challenge2, &HandledConsentRequest{
ID: challenge2,
Remember: true,
RememberFor: int(extendRememberFor),
WasHandled: true,
RequestedAt: time.Now().UTC(),
})
require.NoError(t, err)

require.NoError(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, int(extendRememberFor)))

_, err = m.FindSubjectsGrantedConsentRequests(context.Background(), subject, 100, 0)
require.Error(t, err, ErrNoPreviousConsentFound)
})
})

t.Run("case=revoke-auth-request", func(t *testing.T) {
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
ID: "rev-session-1",
Expand Down
58 changes: 58 additions & 0 deletions persistence/sql/persister_consent.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,64 @@ func (p *Persister) HandleConsentRequest(ctx context.Context, challenge string,
return p.GetConsentRequest(ctx, challenge)
}

func (p *Persister) ExtendConsentRequest(ctx context.Context, scopeStrategy fosite.ScopeStrategy, cr *consent.ConsentRequest, extendBy int) error {
return p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error {
tn := consent.HandledConsentRequest{}.TableName()

var sessionHcr consent.HandledConsentRequest
if err := c.
Where(fmt.Sprintf("r.subject = ? AND r.client_id = ? AND r.login_session_id = ? AND r.skip=FALSE AND (%s.error='{}' AND %s.remember=TRUE)", tn, tn), cr.Subject, cr.ClientID, cr.LoginSessionID.String()).
Join("hydra_oauth2_consent_request AS r", fmt.Sprintf("%s.challenge = r.challenge", tn)).
Order(fmt.Sprintf("%s.requested_at DESC", tn)).
Limit(1).
First(&sessionHcr); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errorsx.WithStack(consent.ErrNoPreviousConsentFound)
}
return sqlcon.HandleError(err)
}

var latestHcr consent.HandledConsentRequest
if err := c.
Where(fmt.Sprintf("r.subject = ? AND r.client_id = ? AND r.skip=FALSE AND (%s.error='{}' AND %s.remember=TRUE)", tn, tn), cr.Subject, cr.ClientID).
Join("hydra_oauth2_consent_request AS r", fmt.Sprintf("%s.challenge = r.challenge", tn)).
Order(fmt.Sprintf("%s.requested_at DESC", tn)).
Limit(1).
First(&latestHcr); err != nil {
return sqlcon.HandleError(err)
}

if err := p.extendHandledConsentRequest(ctx, cr, scopeStrategy, sessionHcr, extendBy); err != nil {
return err
}

if latestHcr.ID != sessionHcr.ID {
if err := p.extendHandledConsentRequest(ctx, cr, scopeStrategy, latestHcr, extendBy); err != nil {
return err
}
}
return nil
})
}

func (p *Persister) extendHandledConsentRequest(ctx context.Context, cr *consent.ConsentRequest, scopeStrategy fosite.ScopeStrategy, hcr consent.HandledConsentRequest, extendBy int) error {
for _, scope := range cr.RequestedScope {
if !scopeStrategy(hcr.GrantedScope, scope) {
return nil
}
}

isConsentRequestExpired := hcr.RememberFor > 0 && hcr.RequestedAt.Add(time.Duration(hcr.RememberFor)*time.Second).Before(time.Now().UTC())
if isConsentRequestExpired {
return nil
}

remainingTime := hcr.RequestedAt.Unix() + int64(hcr.RememberFor) - time.Now().Unix()
hcr.RememberFor = hcr.RememberFor + extendBy - int(remainingTime)

return sqlcon.HandleError(p.Connection(ctx).Update(&hcr))
}

func (p *Persister) VerifyAndInvalidateConsentRequest(ctx context.Context, verifier string) (*consent.HandledConsentRequest, error) {
var r consent.HandledConsentRequest
return &r, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error {
Expand Down
2 changes: 1 addition & 1 deletion persistence/sql/persister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestManagers(t *testing.T) {
t.Run("case=update-two-clients", client.TestHelperUpdateTwoClients(k, m.ClientManager()))
})

t.Run("package=consent/manager="+k, consent.ManagerTests(m.ConsentManager(), m.ClientManager(), m.OAuth2Storage()))
t.Run("package=consent/manager="+k, consent.ManagerTests(m.ConsentManager(), m.ClientManager(), m.ScopeStrategy(), m.OAuth2Storage()))

t.Run("package=consent/janitor="+k, testhelpers.JanitorTests(m.Config(), m.ConsentManager(), m.ClientManager(), m.OAuth2Storage()))

Expand Down

0 comments on commit 3c07b1c

Please sign in to comment.