client/web: use auth ID in browser sessions

Stores ID from tailcfg.WebClientAuthResponse in browser session
data, and uses ID to hit control server /wait endpoint.

No longer need the control url cached, so removed that from Server.
Also added optional timeNow field, initially to manage time from
tests.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-10-19 16:13:40 -04:00
committed by Sonia Appasamy
parent 6ada33db77
commit 1df2d14c8f
2 changed files with 134 additions and 68 deletions

View File

@ -11,7 +11,6 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
@ -19,7 +18,6 @@ import (
"github.com/google/go-cmp/cmp"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/memnet"
"tailscale.com/tailcfg"
@ -412,9 +410,14 @@ func TestServeTailscaleAuth(t *testing.T) {
defer localapi.Close()
go localapi.Serve(lal)
timeNow := time.Now()
oneHourAgo := timeNow.Add(-time.Hour)
sixtyDaysAgo := timeNow.Add(-sessionCookieExpiry * 2)
s := &Server{
lc: &tailscale.LocalClient{Dial: lal.Dial},
tsDebugMode: "full",
timeNow: func() time.Time { return timeNow },
}
successCookie := "ts-cookie-success"
@ -422,7 +425,8 @@ func TestServeTailscaleAuth(t *testing.T) {
ID: successCookie,
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: time.Now(),
Created: oneHourAgo,
AuthID: testAuthPathSuccess,
AuthURL: testControlURL + testAuthPathSuccess,
})
failureCookie := "ts-cookie-failure"
@ -430,7 +434,8 @@ func TestServeTailscaleAuth(t *testing.T) {
ID: failureCookie,
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: time.Now(),
Created: oneHourAgo,
AuthID: testAuthPathError,
AuthURL: testControlURL + testAuthPathError,
})
expiredCookie := "ts-cookie-expired"
@ -438,7 +443,8 @@ func TestServeTailscaleAuth(t *testing.T) {
ID: expiredCookie,
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: time.Now().Add(-sessionCookieExpiry * 2),
Created: sixtyDaysAgo,
AuthID: "/a/old-auth-url",
AuthURL: testControlURL + "/a/old-auth-url",
})
@ -448,19 +454,40 @@ func TestServeTailscaleAuth(t *testing.T) {
query string
wantStatus int
wantResp *authResponse
wantNewCookie bool // new cookie generated
wantNewCookie bool // new cookie generated
wantSession *browserSession // session associated w/ cookie at end of request
}{
{
name: "new-session-created",
wantStatus: http.StatusOK,
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPath},
wantNewCookie: true,
}, {
wantSession: &browserSession{
ID: "GENERATED_ID", // gets swapped for newly created ID by test
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: timeNow,
AuthID: testAuthPath,
AuthURL: testControlURL + testAuthPath,
Authenticated: false,
},
},
{
name: "query-existing-incomplete-session",
cookie: successCookie,
wantStatus: http.StatusOK,
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPathSuccess},
}, {
wantSession: &browserSession{
ID: successCookie,
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: oneHourAgo,
AuthID: testAuthPathSuccess,
AuthURL: testControlURL + testAuthPathSuccess,
Authenticated: false,
},
},
{
name: "transition-to-successful-session",
cookie: successCookie,
// query "wait" indicates the FE wants to make
@ -468,29 +495,70 @@ func TestServeTailscaleAuth(t *testing.T) {
query: "wait=true",
wantStatus: http.StatusOK,
wantResp: &authResponse{OK: true},
}, {
wantSession: &browserSession{
ID: successCookie,
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: oneHourAgo,
AuthID: testAuthPathSuccess,
AuthURL: testControlURL + testAuthPathSuccess,
Authenticated: true,
},
},
{
name: "query-existing-complete-session",
cookie: successCookie,
wantStatus: http.StatusOK,
wantResp: &authResponse{OK: true},
}, {
name: "transition-to-failed-session",
cookie: failureCookie,
query: "wait=true",
wantStatus: http.StatusUnauthorized,
wantResp: nil,
}, {
wantSession: &browserSession{
ID: successCookie,
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: oneHourAgo,
AuthID: testAuthPathSuccess,
AuthURL: testControlURL + testAuthPathSuccess,
Authenticated: true,
},
},
{
name: "transition-to-failed-session",
cookie: failureCookie,
query: "wait=true",
wantStatus: http.StatusUnauthorized,
wantResp: nil,
wantSession: nil, // session deleted
},
{
name: "failed-session-cleaned-up",
cookie: failureCookie,
wantStatus: http.StatusOK,
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPath},
wantNewCookie: true,
}, {
wantSession: &browserSession{
ID: "GENERATED_ID",
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: timeNow,
AuthID: testAuthPath,
AuthURL: testControlURL + testAuthPath,
Authenticated: false,
},
},
{
name: "expired-cookie-gets-new-session",
cookie: expiredCookie,
wantStatus: http.StatusOK,
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPath},
wantNewCookie: true,
wantSession: &browserSession{
ID: "GENERATED_ID",
SrcNode: remoteNode.Node.ID,
SrcUser: user.ID,
Created: timeNow,
AuthID: testAuthPath,
AuthURL: testControlURL + testAuthPath,
Authenticated: false,
},
},
}
for _, tt := range tests {
@ -503,6 +571,8 @@ func TestServeTailscaleAuth(t *testing.T) {
s.serveTailscaleAuth(w, r)
res := w.Result()
defer res.Body.Close()
// Validate response status/data.
if gotStatus := res.StatusCode; tt.wantStatus != gotStatus {
t.Errorf("wrong status; want=%v, got=%v", tt.wantStatus, gotStatus)
}
@ -516,19 +586,35 @@ func TestServeTailscaleAuth(t *testing.T) {
t.Fatal(err)
}
}
if !reflect.DeepEqual(gotResp, tt.wantResp) {
t.Errorf("wrong response; want=%v, got=%v", tt.wantResp, gotResp)
if diff := cmp.Diff(gotResp, tt.wantResp); diff != "" {
t.Errorf("wrong response; (-got+want):%v", diff)
}
// Validate cookie creation.
sessionID := tt.cookie
var gotCookie bool
for _, c := range w.Result().Cookies() {
if c.Name == sessionCookieName {
gotCookie = true
sessionID = c.Value
break
}
}
if gotCookie != tt.wantNewCookie {
t.Errorf("wantNewCookie wrong; want=%v, got=%v", tt.wantNewCookie, gotCookie)
}
// Validate browser session contents.
var gotSesson *browserSession
if s, ok := s.browserSessions.Load(sessionID); ok {
gotSesson = s.(*browserSession)
}
if tt.wantSession != nil && tt.wantSession.ID == "GENERATED_ID" {
// If requested, swap in the generated session ID before
// comparing got/want.
tt.wantSession.ID = sessionID
}
if diff := cmp.Diff(gotSesson, tt.wantSession); diff != "" {
t.Errorf("wrong session; (-got+want):%v", diff)
}
})
}
}
@ -572,14 +658,6 @@ func mockLocalAPI(t *testing.T, whoIs map[string]*apitype.WhoIsResponse, self fu
}
w.Header().Set("Content-Type", "application/json")
return
case "/localapi/v0/prefs":
prefs := ipn.Prefs{ControlURL: testControlURL}
if err := json.NewEncoder(w).Encode(prefs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
return
case "/localapi/v0/debug-web-client": // used by TestServeTailscaleAuth
type reqData struct {
ID string
@ -596,7 +674,7 @@ func mockLocalAPI(t *testing.T, whoIs map[string]*apitype.WhoIsResponse, self fu
}
var resp *tailcfg.WebClientAuthResponse
if data.ID == "" {
resp = &tailcfg.WebClientAuthResponse{URL: testControlURL + testAuthPath}
resp = &tailcfg.WebClientAuthResponse{ID: testAuthPath, URL: testControlURL + testAuthPath}
} else if data.ID == testAuthPathSuccess {
resp = &tailcfg.WebClientAuthResponse{Complete: true}
} else if data.ID == testAuthPathError {