Skip to content

Commit 1f26dc6

Browse files
committed
feat: extend latest and session related consents
1 parent bfb7d62 commit 1f26dc6

File tree

6 files changed

+313
-6
lines changed

6 files changed

+313
-6
lines changed

consent/handler.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,14 @@ func (h *Handler) acceptOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ
671671
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
672672
return
673673
} else if hr.Skip {
674+
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)
675+
var ctx = r.Context()
676+
err = h.r.ConsentManager().ExtendConsentRequest(r.Context(), h.r.Config().GetScopeStrategy(ctx), hr, p.RememberFor)
677+
if err != nil {
678+
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
679+
return
680+
}
681+
}
674682
p.Remember = false
675683
}
676684

consent/handler_test.go

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import (
1313
"testing"
1414
"time"
1515

16-
"github.com/ory/x/pointerx"
17-
1816
"github.com/ory/hydra/consent"
1917
"github.com/ory/hydra/x"
2018
"github.com/ory/x/contextx"
19+
"github.com/ory/x/pointerx"
2120
"github.com/ory/x/sqlxx"
2221

2322
"github.com/ory/hydra/internal"
@@ -214,6 +213,98 @@ func TestGetConsentRequest(t *testing.T) {
214213
}
215214
}
216215

216+
func TestExtendConsentRequest(t *testing.T) {
217+
t.Run("case=extend consent expiry time", func(t *testing.T) {
218+
conf := internal.NewConfigurationWithDefaults()
219+
reg := internal.NewRegistryMemory(t, conf, &contextx.Default{})
220+
h := NewHandler(reg, conf)
221+
r := x.NewRouterAdmin(conf.AdminURL)
222+
h.SetRoutes(r)
223+
ts := httptest.NewServer(r)
224+
defer ts.Close()
225+
226+
c := &http.Client{}
227+
cl := &client.Client{LegacyClientID: "client-1"}
228+
require.NoError(t, reg.ClientManager().CreateClient(context.Background(), cl))
229+
230+
var initialRememberFor time.Duration = 300
231+
var remainingValidTime time.Duration = 100
232+
233+
require.NoError(t, reg.ConsentManager().CreateLoginSession(context.Background(), &LoginSession{
234+
ID: makeID("fk-login-session", "1", "1"),
235+
Subject: "subject-1",
236+
}))
237+
requestedTimeInPast := time.Now().UTC().Add(-(initialRememberFor - remainingValidTime) * time.Second)
238+
require.NoError(t, reg.ConsentManager().CreateLoginRequest(context.Background(), &LoginRequest{
239+
ID: makeID("challenge", "1", "1"),
240+
SessionID: sqlxx.NullString(makeID("fk-login-session", "1", "1")),
241+
Client: cl,
242+
Subject: "subject-1",
243+
RequestedAt: requestedTimeInPast,
244+
}))
245+
require.NoError(t, reg.ConsentManager().CreateConsentRequest(context.Background(), &OAuth2ConsentRequest{
246+
ID: makeID("challenge", "1", "1"),
247+
Subject: "subject-1",
248+
Client: cl,
249+
LoginSessionID: sqlxx.NullString(makeID("fk-login-session", "1", "1")),
250+
LoginChallenge: sqlxx.NullString(makeID("challenge", "1", "1")),
251+
Verifier: makeID("verifier", "1", "1"),
252+
CSRF: "csrf1",
253+
Skip: false,
254+
ACR: "1",
255+
}))
256+
_, err := reg.ConsentManager().HandleConsentRequest(context.Background(), &AcceptOAuth2ConsentRequest{
257+
ID: makeID("challenge", "1", "1"),
258+
Remember: true,
259+
RememberFor: int(initialRememberFor),
260+
WasHandled: true,
261+
HandledAt: sqlxx.NullTime(time.Now().UTC()),
262+
})
263+
require.NoError(t, err)
264+
265+
require.NoError(t, reg.ConsentManager().CreateLoginRequest(context.Background(), &LoginRequest{
266+
ID: makeID("challenge", "1", "2"),
267+
SessionID: sqlxx.NullString(makeID("fk-login-session", "1", "1")),
268+
Verifier: makeID("verifier", "1", "1"),
269+
Client: cl,
270+
RequestedAt: time.Now().UTC(),
271+
Subject: "subject-1",
272+
}))
273+
require.NoError(t, reg.ConsentManager().CreateConsentRequest(context.Background(), &OAuth2ConsentRequest{
274+
ID: makeID("challenge", "1", "2"),
275+
Subject: "subject-1",
276+
Client: cl,
277+
LoginSessionID: sqlxx.NullString(makeID("fk-login-session", "1", "1")),
278+
LoginChallenge: sqlxx.NullString(makeID("challenge", "1", "2")),
279+
Verifier: makeID("verifier", "1", "2"),
280+
CSRF: "csrf2",
281+
Skip: true,
282+
}))
283+
284+
var b bytes.Buffer
285+
var extendRememberFor time.Duration = 300
286+
require.NoError(t, json.NewEncoder(&b).Encode(&AcceptOAuth2ConsentRequest{
287+
Remember: true,
288+
RememberFor: int(extendRememberFor),
289+
}))
290+
291+
req, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+ConsentPath+"/accept?challenge=challenge-1-2", &b)
292+
require.NoError(t, err)
293+
resp, err := c.Do(req)
294+
require.NoError(t, err)
295+
require.EqualValues(t, 200, resp.StatusCode)
296+
297+
crs, err := reg.ConsentManager().FindSubjectsGrantedConsentRequests(context.Background(), "subject-1", 100, 0)
298+
require.NoError(t, err)
299+
require.NotNil(t, crs)
300+
require.EqualValues(t, 1, len(crs))
301+
expectedRememberFor := int(initialRememberFor + extendRememberFor - remainingValidTime)
302+
cr := crs[0]
303+
require.EqualValues(t, "challenge-1-1", cr.ID)
304+
require.InDelta(t, expectedRememberFor, cr.RememberFor, 1)
305+
})
306+
}
307+
217308
func TestGetLoginRequestWithDuplicateAccept(t *testing.T) {
218309
t.Run("Test get login request with duplicate accept", func(t *testing.T) {
219310
challenge := "challenge"

consent/manager.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"context"
88
"time"
99

10+
"github.com/ory/fosite"
11+
1012
"github.com/gofrs/uuid"
1113

1214
"github.com/ory/hydra/client"
@@ -27,6 +29,7 @@ type Manager interface {
2729
CreateConsentRequest(ctx context.Context, req *OAuth2ConsentRequest) error
2830
GetConsentRequest(ctx context.Context, challenge string) (*OAuth2ConsentRequest, error)
2931
HandleConsentRequest(ctx context.Context, r *AcceptOAuth2ConsentRequest) (*OAuth2ConsentRequest, error)
32+
ExtendConsentRequest(ctx context.Context, scopeStrategy fosite.ScopeStrategy, req *OAuth2ConsentRequest, extendBy int) error
3033
RevokeSubjectConsentSession(ctx context.Context, user string) error
3134
RevokeSubjectClientConsentSession(ctx context.Context, user, client string) error
3235

consent/manager_test_helpers.go

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func TestHelperNID(t1ClientManager client.Manager, t1ValidNID Manager, t2Invalid
294294
}
295295
}
296296

297-
func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.FositeStorer, network string, parallel bool) func(t *testing.T) {
297+
func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.FositeStorer, scopeStrategy fosite.ScopeStrategy, network string, parallel bool) func(t *testing.T) {
298298
lr := make(map[string]*LoginRequest)
299299

300300
return func(t *testing.T) {
@@ -534,6 +534,129 @@ func ManagerTests(m Manager, clientManager client.Manager, fositeManager x.Fosit
534534
}
535535
})
536536

537+
t.Run("case=extend consent request", func(t *testing.T) {
538+
cl := &client.Client{LegacyClientID: "client-1"}
539+
_ = clientManager.CreateClient(context.Background(), cl)
540+
consentFlow := func(subject, sessionId, challenge string, rememberFor time.Duration, requestedAt time.Time, requestedScope string, skip bool) *OAuth2ConsentRequest {
541+
require.NoError(t, m.CreateLoginRequest(context.Background(), &LoginRequest{
542+
ID: makeID("challenge", network, challenge),
543+
SessionID: sqlxx.NullString(makeID("fk-login-session", network, sessionId)),
544+
Client: cl,
545+
Subject: subject,
546+
Verifier: uuid.New().String(),
547+
RequestedAt: requestedAt,
548+
RequestedScope: []string{requestedScope},
549+
}))
550+
551+
require.NoError(t, m.CreateConsentRequest(context.Background(), &OAuth2ConsentRequest{
552+
ID: makeID("challenge", network, challenge),
553+
Client: cl,
554+
Subject: subject,
555+
LoginSessionID: sqlxx.NullString(makeID("fk-login-session", network, sessionId)),
556+
LoginChallenge: sqlxx.NullString(makeID("challenge", network, challenge)),
557+
Skip: skip,
558+
Verifier: uuid.New().String(),
559+
CSRF: "csrf1",
560+
}))
561+
cr, err := m.HandleConsentRequest(context.Background(), &AcceptOAuth2ConsentRequest{
562+
ID: makeID("challenge", network, challenge),
563+
Remember: true,
564+
RememberFor: int(rememberFor),
565+
WasHandled: true,
566+
HandledAt: sqlxx.NullTime(time.Now().UTC()),
567+
GrantedScope: []string{"scope-a"},
568+
})
569+
require.NoError(t, err)
570+
return cr
571+
}
572+
573+
t.Run("case=extend session related and latest consent expiry times", func(t *testing.T) {
574+
var rememberForSession1 time.Duration = 300
575+
var remainingValidTimeSession1 time.Duration = 100
576+
var rememberForSession2 time.Duration = 300
577+
var remainingValidTimeSession2 time.Duration = 150
578+
var extendRememberFor time.Duration = 1000
579+
requestedAt1 := time.Now().UTC().Round(time.Second).Add(-(rememberForSession1 - remainingValidTimeSession1) * time.Second)
580+
requestedAt2 := time.Now().UTC().Round(time.Second).Add(-(rememberForSession2 - remainingValidTimeSession2) * time.Second)
581+
requestedAt3 := time.Now().UTC()
582+
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
583+
ID: makeID("fk-login-session", network, "ec1"),
584+
Subject: "subject-1",
585+
}))
586+
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
587+
ID: makeID("fk-login-session", network, "ec2"),
588+
Subject: "subject-1",
589+
}))
590+
consentFlow("subject-1", "ec1", "c1", rememberForSession1, requestedAt1, "scope-a", false)
591+
consentFlow("subject-1", "ec2", "c2", rememberForSession2, requestedAt2, "scope-a", false)
592+
cr := consentFlow("subject-1", "ec1", "c3", extendRememberFor, requestedAt3, "scope-a", true)
593+
594+
require.NoError(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, int(extendRememberFor)))
595+
596+
crs, err := m.FindSubjectsGrantedConsentRequests(context.Background(), "subject-1", 100, 0)
597+
require.NoError(t, err)
598+
require.EqualValues(t, 2, len(crs))
599+
crSession := crs[1]
600+
require.EqualValues(t, makeID("challenge", network, "c1"), crSession.ID)
601+
expectedExtendedRememberFor1 := int(rememberForSession1 + extendRememberFor - remainingValidTimeSession1)
602+
require.InDelta(t, expectedExtendedRememberFor1, crSession.RememberFor, 1)
603+
crLatest := crs[0]
604+
require.EqualValues(t, makeID("challenge", network, "c2"), crLatest.ID)
605+
expectedExtendedRememberFor2 := int(rememberForSession2 + extendRememberFor - remainingValidTimeSession2)
606+
require.InDelta(t, expectedExtendedRememberFor2, crLatest.RememberFor, 1)
607+
})
608+
609+
t.Run("case=no previous consent found", func(t *testing.T) {
610+
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
611+
ID: makeID("fk-login-session", network, "ec3"),
612+
Subject: "subject-1",
613+
}))
614+
cr := consentFlow("subject-1", "ec3", "c4", 300, time.Now().UTC(), "scope-a", true)
615+
616+
require.ErrorIs(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, 1000), ErrNoPreviousConsentFound)
617+
})
618+
619+
t.Run("case=invalid requested scope", func(t *testing.T) {
620+
var rememberForSession1 time.Duration = 300
621+
var remainingValidTimeSession1 time.Duration = 100
622+
requestedAt1 := time.Now().UTC().Round(time.Second).Add(-(rememberForSession1 - remainingValidTimeSession1) * time.Second)
623+
requestedAt2 := time.Now().UTC()
624+
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
625+
ID: makeID("fk-login-session", network, "ec4"),
626+
Subject: "subject-2",
627+
}))
628+
consentFlow("subject-2", "ec4", "c5", 300, requestedAt1, "scope-a", false)
629+
cr := consentFlow("subject-2", "ec4", "c6", 300, requestedAt2, "scope-b", true)
630+
631+
require.NoError(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, 1000))
632+
633+
crs, err := m.FindSubjectsGrantedConsentRequests(context.Background(), "subject-2", 10, 0)
634+
require.NoError(t, err)
635+
require.EqualValues(t, 1, len(crs))
636+
cr1 := crs[0]
637+
require.EqualValues(t, makeID("challenge", network, "c5"), cr1.ID)
638+
require.EqualValues(t, 300, cr1.RememberFor)
639+
})
640+
641+
t.Run("case=initial consent request expired", func(t *testing.T) {
642+
var rememberForSession1 time.Duration = 300
643+
var remainingValidTimeSession1 time.Duration = 0
644+
requestedAtExpired := time.Now().UTC().Round(time.Second).Add(-(rememberForSession1 - remainingValidTimeSession1) * time.Second)
645+
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
646+
ID: makeID("fk-login-session", network, "ec5"),
647+
Subject: "subject-3",
648+
}))
649+
consentFlow("subject-3", "ec5", "c7", 300, requestedAtExpired, "scope-a", false)
650+
time.Sleep(time.Second)
651+
cr := consentFlow("subject-3", "ec5", "c8", 300, time.Now().UTC(), "scope-a", true)
652+
653+
require.NoError(t, m.ExtendConsentRequest(context.Background(), scopeStrategy, cr, 1000))
654+
655+
_, err := m.FindSubjectsGrantedConsentRequests(context.Background(), "subject-3", 100, 0)
656+
require.Error(t, err, ErrNoPreviousConsentFound)
657+
})
658+
})
659+
537660
t.Run("case=revoke-auth-request", func(t *testing.T) {
538661
require.NoError(t, m.CreateLoginSession(context.Background(), &LoginSession{
539662
ID: makeID("rev-session", network, "-1"),

persistence/sql/persister_consent.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,88 @@ func (p *Persister) HandleConsentRequest(ctx context.Context, r *consent.AcceptO
283283
return p.GetConsentRequest(ctx, r.ID)
284284
}
285285

286+
func (p *Persister) ExtendConsentRequest(ctx context.Context, scopeStrategy fosite.ScopeStrategy, cr *consent.OAuth2ConsentRequest, extendBy int) error {
287+
return p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error {
288+
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ExtendConsentRequest")
289+
defer span.End()
290+
291+
sessionFlow := &flow.Flow{}
292+
if err := c.
293+
Where(
294+
strings.TrimSpace(fmt.Sprintf(`
295+
(state = %d OR state = %d) AND
296+
subject = ? AND
297+
client_id = ? AND
298+
login_session_id = ? AND
299+
consent_skip=FALSE AND
300+
consent_error='{}' AND
301+
consent_remember=TRUE AND
302+
nid = ?`, flow.FlowStateConsentUsed, flow.FlowStateConsentUnused,
303+
)),
304+
cr.Subject, cr.ClientID, cr.LoginSessionID.String(), p.NetworkID(ctx)).
305+
Order("requested_at DESC").
306+
Limit(1).
307+
First(sessionFlow); err != nil {
308+
if errors.Is(err, sql.ErrNoRows) {
309+
return errorsx.WithStack(consent.ErrNoPreviousConsentFound)
310+
}
311+
return sqlcon.HandleError(err)
312+
}
313+
314+
latestFlow := &flow.Flow{}
315+
if err := c.
316+
Where(
317+
strings.TrimSpace(fmt.Sprintf(`
318+
(state = %d OR state = %d) AND
319+
subject = ? AND
320+
client_id = ? AND
321+
consent_skip=FALSE AND
322+
consent_error='{}' AND
323+
consent_remember=TRUE AND
324+
nid = ?`, flow.FlowStateConsentUsed, flow.FlowStateConsentUnused,
325+
)),
326+
cr.Subject, cr.ClientID, p.NetworkID(ctx)).
327+
Order("requested_at DESC").
328+
Limit(1).
329+
First(latestFlow); err != nil {
330+
return sqlcon.HandleError(err)
331+
}
332+
333+
if err := p.extendHandledConsentRequest(ctx, cr, scopeStrategy, sessionFlow, extendBy); err != nil {
334+
return err
335+
}
336+
337+
if latestFlow.ID != sessionFlow.ID {
338+
if err := p.extendHandledConsentRequest(ctx, cr, scopeStrategy, latestFlow, extendBy); err != nil {
339+
return err
340+
}
341+
}
342+
return nil
343+
})
344+
}
345+
346+
func (p *Persister) extendHandledConsentRequest(ctx context.Context, cr *consent.OAuth2ConsentRequest, scopeStrategy fosite.ScopeStrategy, f *flow.Flow, extendBy int) error {
347+
for _, scope := range cr.RequestedScope {
348+
if !scopeStrategy(f.GrantedScope, scope) {
349+
return nil
350+
}
351+
}
352+
hcr := f.GetHandledConsentRequest()
353+
if isConsentRequestExpired := hcr.RememberFor > 0 && hcr.RequestedAt.Add(time.Duration(hcr.RememberFor)*time.Second).Before(time.Now().UTC()); isConsentRequestExpired {
354+
return nil
355+
}
356+
remainingTime := hcr.RequestedAt.Unix() + int64(hcr.RememberFor) - time.Now().Unix()
357+
extendedRememberFor := hcr.RememberFor + extendBy - int(remainingTime)
358+
f.ConsentRememberFor = &extendedRememberFor
359+
360+
_, err := p.UpdateWithNetwork(ctx, f)
361+
if err != nil {
362+
return sqlcon.HandleError(err)
363+
} else {
364+
return nil
365+
}
366+
}
367+
286368
func (p *Persister) VerifyAndInvalidateConsentRequest(ctx context.Context, verifier string) (*consent.AcceptOAuth2ConsentRequest, error) {
287369
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.VerifyAndInvalidateConsentRequest")
288370
defer span.End()

persistence/sql/persister_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ func testRegistry(t *testing.T, ctx context.Context, k string, t1 driver.Registr
5151
if k == "memory" || k == "mysql" || k == "cockroach" { // TODO enable parallel tests for cockroach once we configure the cockroach integration test server to support retry
5252
parallel = false
5353
}
54-
55-
t.Run("package=consent/manager="+k, consent.ManagerTests(t1.ConsentManager(), t1.ClientManager(), t1.OAuth2Storage(), "t1", parallel))
56-
t.Run("package=consent/manager="+k, consent.ManagerTests(t2.ConsentManager(), t2.ClientManager(), t2.OAuth2Storage(), "t2", parallel))
54+
scopeStrategy := t1.Config().GetScopeStrategy(ctx)
55+
t.Run("package=consent/manager="+k, consent.ManagerTests(t1.ConsentManager(), t1.ClientManager(), t1.OAuth2Storage(), scopeStrategy, "t1", parallel))
56+
t.Run("package=consent/manager="+k, consent.ManagerTests(t2.ConsentManager(), t2.ClientManager(), t2.OAuth2Storage(), scopeStrategy, "t2", parallel))
5757

5858
t.Run("parallel-boundary", func(t *testing.T) {
5959
t.Run("package=consent/janitor="+k, testhelpers.JanitorTests(t1.Config(), t1.ConsentManager(), t1.ClientManager(), t1.OAuth2Storage(), "t1", parallel))

0 commit comments

Comments
 (0)