From be084737da4c11f59de9f9674ee8670a02e8b587 Mon Sep 17 00:00:00 2001 From: Ryan Mello Date: Wed, 12 Aug 2026 04:31:00 +0100 Subject: [PATCH] revert(jwt): restore the 30-day token lifetime Reverts the expiryDuration change from 24h back to 30 days, by team decision. The one-day value bounded stale authorization for a client that stays online indefinitely, which is a real thing to want. It rested on the premise that conforming clients rotate at half-life -- true of the sdk only since the half-life refresh landed on 2026-08-06, and not true of anything else holding a token: an app build older than that, or any client issued a token it never refreshes. Those clients do not fail loudly at the deadline. They stay connected, keep accepting connections, and carry nothing -- healthy from every angle except an egress probe. Measured on beta by provider age at probe time: <6h 95% probe success 6-12h 95% 12-20h 58% 20-24h 14% 24-30h 0% (104 attempts) >30h 0% (166 attempts) A fleet-wide blackhole on a 24h clock, invisible to everything that was not probing egress. This does not repair anything on its own. A token already issued keeps its original expiry, so a client that has gone dark stays dark until it re-authenticates; the longer lifetime only applies to tokens minted from here. Nor does it fix the non-refreshing client -- it makes the deadline rare enough to be noticed rather than routine. The durable protections are elsewhere and stay in place: the sdk's half-life rotation, and the server refusing to advertise a provider whose egress health has aged out (ProviderEgressHealthMaxAge). Shortening this again should wait until a client that ignores the deadline is the exception rather than the rule. The lifetime test is updated rather than deleted, and says why it asserts a value at all: clients schedule rotation against this number, and the two have already fallen out of step once. (cherry picked from commit dce6442a6ba4d99fd7704cdfc4bcc5b77ffce446) --- jwt/by_jwt.go | 27 +++++++++++++++++++++++---- jwt/by_jwt_test.go | 8 ++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/jwt/by_jwt.go b/jwt/by_jwt.go index 2f85ee29..0e814334 100644 --- a/jwt/by_jwt.go +++ b/jwt/by_jwt.go @@ -31,10 +31,29 @@ var byJwtTlsKeyPaths = sync.OnceValue(func() []string { }) const ( - // A one-day credential bounds stale authorization even when a client stays - // online indefinitely. SDK APIs refresh at half-life and retry before this - // deadline, so conforming clients rotate without a protocol change. - expiryDuration = 24 * time.Hour + // Reverted from 24h to 30 days by team decision. + // + // The one-day value bounded stale authorization for a client that stays + // online indefinitely, which is a real thing to want. It rested on + // "conforming clients rotate at half-life" -- and that is true of the sdk + // only since the half-life refresh landed on 2026-08-06. It is not true of + // anything else holding a token: an app build older than that, or any client + // issued a token it does not refresh. + // + // Those clients do not fail loudly at the deadline. They stay connected and + // keep accepting connections while carrying nothing, so they look healthy + // from every angle except an egress probe. Measured on beta: 95% probe + // success under 12h of client age, 14% at 20-24h, and 0% past 24h across 270 + // attempts -- a fleet-wide blackhole on a 24h clock, invisible to everything + // that was not probing egress. + // + // 30 days does not fix the non-refreshing client, it only makes the deadline + // rare enough to notice. The durable protections are elsewhere and stay in + // place: the sdk's half-life rotation, and the server refusing to advertise + // a provider whose egress health has aged out (ProviderEgressHealthMaxAge). + // Shortening this again should wait until a client that ignores the deadline + // is the exception rather than the rule. + expiryDuration = 30 * 24 * time.Hour clockLeeway = 30 * time.Second ByJwtIssuer = "urnetwork:byjwt" diff --git a/jwt/by_jwt_test.go b/jwt/by_jwt_test.go index 908ed34d..18407a77 100644 --- a/jwt/by_jwt_test.go +++ b/jwt/by_jwt_test.go @@ -112,10 +112,14 @@ func TestByJwtRegisteredClaimsAreEnforced(t *testing.T) { }) } -func TestByJwtLifetimeIsOneDay(t *testing.T) { +// The lifetime is asserted rather than assumed because clients schedule their +// own rotation against it, and the two have already fallen out of step once: a +// move to 24h left every non-refreshing client silently dark after a day, while +// staying connected and accepting traffic it could not carry. +func TestByJwtLifetimeIsThirtyDays(t *testing.T) { claims := NewByJwt(server.NewId(), server.NewId(), "test", false, false) lifetime := claims.ExpiresAt.Time.Sub(claims.IssuedAt.Time) - connect.AssertEqual(t, lifetime, 24*time.Hour) + connect.AssertEqual(t, lifetime, 30*24*time.Hour) } // TestByJwtKid covers the `kid` key-selection behavior: a freshly signed token