Skip to content

Commit 76913b0

Browse files
committed
fix: add configurable ttl.logout_token and optional exp claim
1 parent 5306aaf commit 76913b0

4 files changed

Lines changed: 55 additions & 3 deletions

File tree

consent/strategy_default.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -711,14 +711,20 @@ func (s *defaultStrategy) executeBackChannelLogout(ctx context.Context, r *http.
711711
// s.r.ConsentManager().GetForcedObfuscatedLoginSession(context.Background(), subject, <missing>)
712712
// sub := s.obfuscateSubjectIdentifier(c, subject, )
713713

714-
t, _, err := s.r.OpenIDJWTSigner().Generate(ctx, jwt.MapClaims{
714+
now := time.Now().UTC()
715+
claims := jwt.MapClaims{
715716
"iss": s.r.Config().IssuerURL(ctx).String(),
716717
"aud": []string{c.ID},
717-
"iat": time.Now().UTC().Unix(),
718+
"iat": now.Unix(),
718719
"jti": uuid.New(),
719720
"events": map[string]struct{}{"http://schemas.openid.net/event/backchannel-logout": {}},
720721
"sid": sid,
721-
}, &jwt.Headers{
722+
}
723+
if logoutTokenLifespan := s.r.Config().GetLogoutTokenLifespan(ctx); logoutTokenLifespan > 0 {
724+
claims["exp"] = now.Add(logoutTokenLifespan).Unix()
725+
}
726+
727+
t, _, err := s.r.OpenIDJWTSigner().Generate(ctx, claims, &jwt.Headers{
722728
Extra: map[string]interface{}{"kid": openIDKeyID},
723729
})
724730
if err != nil {

consent/strategy_logout_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ func TestLogoutFlows(t *testing.T) {
332332
assert.EqualValues(t, <-sid, logoutToken.Get("sid").String(), logoutToken.Raw)
333333
assert.Empty(t, logoutToken.Get("sub").String(), logoutToken.Raw) // The sub claim should be empty because it doesn't work with forced obfuscation and thus we can't easily recover it.
334334
assert.Empty(t, logoutToken.Get("nonce").String(), logoutToken.Raw)
335+
assert.False(t, logoutToken.Get("exp").Exists(), "exp claim should not be present when ttl.logout_token is not set")
335336
})
336337

337338
t.Run("method=get", testExpectPostLogoutPage(createBrowserWithSession(t, c), http.MethodGet, url.Values{}, defaultRedirectedMessage))
@@ -342,6 +343,36 @@ func TestLogoutFlows(t *testing.T) {
342343
backChannelWG.Wait() // we want to ensure that all back channels have been called!
343344
})
344345

346+
t.Run("case=should include exp claim in logout token when ttl.logout_token is set", func(t *testing.T) {
347+
require.NoError(t, reg.Config().Source(ctx).Set(config.KeyLogoutTokenLifespan, "2m"))
348+
t.Cleanup(func() {
349+
require.NoError(t, reg.Config().Source(ctx).Set(config.KeyLogoutTokenLifespan, "0s"))
350+
})
351+
352+
sid := acceptLoginAsAndWatchSid(t, subject)
353+
354+
logoutWg := newWg(2)
355+
setupCheckAndAcceptLogoutHandler(t, logoutWg, nil)
356+
357+
backChannelWG := newWg(2)
358+
c := createClientWithBackchannelLogout(t, backChannelWG, func(t *testing.T, logoutToken gjson.Result) {
359+
assert.EqualValues(t, <-sid, logoutToken.Get("sid").String(), logoutToken.Raw)
360+
assert.True(t, logoutToken.Get("exp").Exists(), "exp claim should be present when ttl.logout_token is set")
361+
362+
iat := logoutToken.Get("iat").Int()
363+
exp := logoutToken.Get("exp").Int()
364+
diff := exp - iat
365+
assert.InDelta(t, 120, diff, 2, "exp should be approximately iat + 120 seconds")
366+
})
367+
368+
t.Run("method=get", testExpectPostLogoutPage(createBrowserWithSession(t, c), http.MethodGet, url.Values{}, defaultRedirectedMessage))
369+
370+
t.Run("method=post", testExpectPostLogoutPage(createBrowserWithSession(t, c), http.MethodPost, url.Values{}, defaultRedirectedMessage))
371+
372+
logoutWg.Wait()
373+
backChannelWG.Wait()
374+
})
375+
345376
// Only do GET requests from here on out, POST should be tested enough to ensure that it is working fine already.
346377

347378
t.Run("case=should fail several flows when id_token_hint is invalid", func(t *testing.T) {

driver/config/provider.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const (
7575
KeyIDTokenLifespan = "ttl.id_token" // #nosec G101
7676
KeyAuthCodeLifespan = "ttl.auth_code"
7777
KeyDeviceAndUserCodeLifespan = "ttl.device_user_code"
78+
KeyLogoutTokenLifespan = "ttl.logout_token" // #nosec G101
7879
KeyAuthenticationSessionLifespan = "ttl.authentication_session"
7980
KeyScopeStrategy = "strategies.scope"
8081
KeyGetCookieSecrets = "secrets.cookie"
@@ -414,6 +415,11 @@ func (p *DefaultProvider) GetDeviceAndUserCodeLifespan(ctx context.Context) time
414415
return p.p.DurationF(KeyDeviceAndUserCodeLifespan, time.Minute*15)
415416
}
416417

418+
// GetLogoutTokenLifespan returns the logout_token lifespan. Defaults to 0 (no exp claim).
419+
func (p *DefaultProvider) GetLogoutTokenLifespan(ctx context.Context) time.Duration {
420+
return p.p.DurationF(KeyLogoutTokenLifespan, 0)
421+
}
422+
417423
// GetAuthenticationSessionLifespan returns the authentication_session lifespan.
418424
func (p *DefaultProvider) GetAuthenticationSessionLifespan(ctx context.Context) time.Duration {
419425
lifespan := p.p.Duration(KeyAuthenticationSessionLifespan)

spec/config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,15 @@
726726
"$ref": "#/definitions/duration"
727727
}
728728
]
729+
},
730+
"logout_token": {
731+
"description": "Configures how long logout tokens are valid. If set to 0 or left unset, no exp claim will be added to the logout token (preserving backward compatibility). The OpenID Connect Back-Channel Logout specification recommends a value of at most two minutes (e.g. \"2m\").",
732+
"type": "string",
733+
"allOf": [
734+
{
735+
"$ref": "#/definitions/duration"
736+
}
737+
]
729738
}
730739
}
731740
},

0 commit comments

Comments
 (0)