From 6c79f55d4890326edd468589919c1ed74caf4623 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Thu, 8 Feb 2024 13:04:01 -0800 Subject: [PATCH] ipnlocal: force-regen new authURL when it is too old (#10971) Fixes tailscale/support-escalations#23. authURLs returned by control expire after 1 hour from creation. Customer reported that the Tailscale client on macOS would sending users to a stale authentication page when clicking on the `Login...` menu item. This can happen when clicking on Login after leaving the device unattended for several days. The device key expires, leading to the creation of a new authURL, however the client doesn't keep track of when the authURL was created. Meaning that `login-interactive` would send the user to an authURL that had expired server-side a long time before. This PR ensures that whenever `login-interactive` is called via LocalAPI, an authURL that is too old won't be used. We force control to give us a new authURL whenever it's been more than 30 minutes since the last authURL was sent down from control. Apply suggestions from code review Set interval to 6 days and 23 hours Signed-off-by: Andrea Gottardo Signed-off-by: Andrea Gottardo --- ipn/ipnlocal/local.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 29e348b05..73429a127 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -241,8 +241,9 @@ type LocalBackend struct { endpoints []tailcfg.Endpoint blocked bool keyExpired bool - authURL string // cleared on Notify - authURLSticky string // not cleared on Notify + authURL string // cleared on Notify + authURLSticky string // not cleared on Notify + authURLTime time.Time // when the authURL was received from the control server interact bool egg bool prevIfState *interfaces.State @@ -1096,6 +1097,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control if st.URL != "" { b.authURL = st.URL b.authURLSticky = st.URL + b.authURLTime = b.clock.Now() } if (wasBlocked || b.seamlessRenewalEnabled()) && st.LoginFinished() { // Interactive login finished successfully (URL visited). @@ -2784,11 +2786,15 @@ func (b *LocalBackend) StartLoginInteractive() { b.assertClientLocked() b.interact = true url := b.authURL + timeSinceAuthURLCreated := b.clock.Since(b.authURLTime) cc := b.cc b.mu.Unlock() b.logf("StartLoginInteractive: url=%v", url != "") - if url != "" { + // Only use an authURL if it was sent down from control in the last + // 6 days and 23 hours. Avoids using a stale URL that is no longer valid + // server-side. Server-side URLs expire after 7 days. + if url != "" && timeSinceAuthURLCreated < ((7*24*time.Hour)-(1*time.Hour)) { b.popBrowserAuthNow() } else { cc.Login(nil, b.loginFlags|controlclient.LoginInteractive) @@ -4166,6 +4172,7 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) { if newState == ipn.Running { b.authURL = "" b.authURLSticky = "" + b.authURLTime = time.Time{} } else if oldState == ipn.Running { // Transitioning away from running. b.closePeerAPIListenersLocked() @@ -4408,6 +4415,7 @@ func (b *LocalBackend) ResetForClientDisconnect() { b.keyExpired = false b.authURL = "" b.authURLSticky = "" + b.authURLTime = time.Time{} b.activeLogin = "" b.setAtomicValuesFromPrefsLocked(ipn.PrefsView{}) b.enterStateLockedOnEntry(ipn.Stopped)