@ -408,7 +408,7 @@ func TestAuthorizeRequest(t *testing.T) {
}
}
}
}
func TestServe Tailscale Auth( t * testing . T ) {
func TestServe Auth( t * testing . T ) {
user := & tailcfg . UserProfile { ID : tailcfg . UserID ( 1 ) }
user := & tailcfg . UserProfile { ID : tailcfg . UserID ( 1 ) }
self := & ipnstate . PeerStatus { ID : "self" , UserID : user . ID }
self := & ipnstate . PeerStatus { ID : "self" , UserID : user . ID }
remoteNode := & apitype . WhoIsResponse { Node : & tailcfg . Node { ID : 1 } , UserProfile : user }
remoteNode := & apitype . WhoIsResponse { Node : & tailcfg . Node { ID : 1 } , UserProfile : user }
@ -462,18 +462,29 @@ func TestServeTailscaleAuth(t *testing.T) {
} )
} )
tests := [ ] struct {
tests := [ ] struct {
name string
name string
cookie string
query string
cookie string // cookie attached to request
wantStatus int
wantNewCookie bool // want new cookie generated during request
wantResp * authResponse
wantSession * browserSession // session associated w/ cookie after request
wantNewCookie bool // new cookie generated
wantSession * browserSession // session associated w/ cookie at end of request
path string
wantStatus int
wantResp any
} {
} {
{
{
name : "new-session-created" ,
name : "no-session" ,
path : "/api/auth" ,
wantStatus : http . StatusOK ,
wantStatus : http . StatusOK ,
wantResp : & authResponse { OK : false , AuthURL : testControlURL + testAuthPath } ,
wantResp : & authResponse { OK : false , AuthNeeded : tailscaleAuth } ,
wantNewCookie : false ,
wantSession : nil ,
} ,
{
name : "new-session" ,
path : "/api/auth/session/new" ,
wantStatus : http . StatusOK ,
wantResp : & newSessionAuthResponse { AuthURL : testControlURL + testAuthPath } ,
wantNewCookie : true ,
wantNewCookie : true ,
wantSession : & browserSession {
wantSession : & browserSession {
ID : "GENERATED_ID" , // gets swapped for newly created ID by test
ID : "GENERATED_ID" , // gets swapped for newly created ID by test
@ -487,9 +498,10 @@ func TestServeTailscaleAuth(t *testing.T) {
} ,
} ,
{
{
name : "query-existing-incomplete-session" ,
name : "query-existing-incomplete-session" ,
path : "/api/auth" ,
cookie : successCookie ,
cookie : successCookie ,
wantStatus : http . StatusOK ,
wantStatus : http . StatusOK ,
wantResp : & authResponse { OK : false , Auth URL: testControlURL + testAuthPathSuccess } ,
wantResp : & authResponse { OK : false , Auth Needed: tailscaleAuth } ,
wantSession : & browserSession {
wantSession : & browserSession {
ID : successCookie ,
ID : successCookie ,
SrcNode : remoteNode . Node . ID ,
SrcNode : remoteNode . Node . ID ,
@ -501,13 +513,27 @@ func TestServeTailscaleAuth(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
name : "transition-to-successful-session" ,
name : "existing-session-used" ,
cookie : successCookie ,
path : "/api/auth/session/new" , // should not create new session
// query "wait" indicates the FE wants to make
cookie : successCookie ,
// local api call to wait until session completed.
query : "wait=true" ,
wantStatus : http . StatusOK ,
wantStatus : http . StatusOK ,
wantResp : & authResponse { OK : true } ,
wantResp : & newSessionAuthResponse { 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" ,
path : "/api/auth/session/wait" ,
cookie : successCookie ,
wantStatus : http . StatusOK ,
wantResp : nil ,
wantSession : & browserSession {
wantSession : & browserSession {
ID : successCookie ,
ID : successCookie ,
SrcNode : remoteNode . Node . ID ,
SrcNode : remoteNode . Node . ID ,
@ -520,6 +546,7 @@ func TestServeTailscaleAuth(t *testing.T) {
} ,
} ,
{
{
name : "query-existing-complete-session" ,
name : "query-existing-complete-session" ,
path : "/api/auth" ,
cookie : successCookie ,
cookie : successCookie ,
wantStatus : http . StatusOK ,
wantStatus : http . StatusOK ,
wantResp : & authResponse { OK : true } ,
wantResp : & authResponse { OK : true } ,
@ -535,17 +562,18 @@ func TestServeTailscaleAuth(t *testing.T) {
} ,
} ,
{
{
name : "transition-to-failed-session" ,
name : "transition-to-failed-session" ,
path : "/api/auth/session/wait" ,
cookie : failureCookie ,
cookie : failureCookie ,
query : "wait=true" ,
wantStatus : http . StatusUnauthorized ,
wantStatus : http . StatusUnauthorized ,
wantResp : nil ,
wantResp : nil ,
wantSession : nil , // session deleted
wantSession : nil , // session deleted
} ,
} ,
{
{
name : "failed-session-cleaned-up" ,
name : "failed-session-cleaned-up" ,
path : "/api/auth/session/new" ,
cookie : failureCookie ,
cookie : failureCookie ,
wantStatus : http . StatusOK ,
wantStatus : http . StatusOK ,
wantResp : & authResponse{ OK : false , AuthURL : testControlURL + testAuthPath } ,
wantResp : & newSessionAuthResponse{ AuthURL : testControlURL + testAuthPath } ,
wantNewCookie : true ,
wantNewCookie : true ,
wantSession : & browserSession {
wantSession : & browserSession {
ID : "GENERATED_ID" ,
ID : "GENERATED_ID" ,
@ -559,9 +587,10 @@ func TestServeTailscaleAuth(t *testing.T) {
} ,
} ,
{
{
name : "expired-cookie-gets-new-session" ,
name : "expired-cookie-gets-new-session" ,
path : "/api/auth/session/new" ,
cookie : expiredCookie ,
cookie : expiredCookie ,
wantStatus : http . StatusOK ,
wantStatus : http . StatusOK ,
wantResp : & authResponse{ OK : false , AuthURL : testControlURL + testAuthPath } ,
wantResp : & newSessionAuthResponse{ AuthURL : testControlURL + testAuthPath } ,
wantNewCookie : true ,
wantNewCookie : true ,
wantSession : & browserSession {
wantSession : & browserSession {
ID : "GENERATED_ID" ,
ID : "GENERATED_ID" ,
@ -576,12 +605,11 @@ func TestServeTailscaleAuth(t *testing.T) {
}
}
for _ , tt := range tests {
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
t . Run ( tt . name , func ( t * testing . T ) {
r := httptest . NewRequest ( "GET" , "/api/auth" , nil )
r := httptest . NewRequest ( "GET" , tt . path , nil )
r . URL . RawQuery = tt . query
r . RemoteAddr = remoteIP
r . RemoteAddr = remoteIP
r . AddCookie ( & http . Cookie { Name : sessionCookieName , Value : tt . cookie } )
r . AddCookie ( & http . Cookie { Name : sessionCookieName , Value : tt . cookie } )
w := httptest . NewRecorder ( )
w := httptest . NewRecorder ( )
s . serve APIAuth ( w , r )
s . serve ( w , r )
res := w . Result ( )
res := w . Result ( )
defer res . Body . Close ( )
defer res . Body . Close ( )
@ -589,17 +617,20 @@ func TestServeTailscaleAuth(t *testing.T) {
if gotStatus := res . StatusCode ; tt . wantStatus != gotStatus {
if gotStatus := res . StatusCode ; tt . wantStatus != gotStatus {
t . Errorf ( "wrong status; want=%v, got=%v" , tt . wantStatus , gotStatus )
t . Errorf ( "wrong status; want=%v, got=%v" , tt . wantStatus , gotStatus )
}
}
var gotResp * authResponse
var gotResp string
if res . StatusCode == http . StatusOK {
if res . StatusCode == http . StatusOK {
body , err := io . ReadAll ( res . Body )
body , err := io . ReadAll ( res . Body )
if err != nil {
if err != nil {
t . Fatal ( err )
t . Fatal ( err )
}
}
if err := json . Unmarshal ( body , & gotResp ) ; err != nil {
gotResp = strings . Trim ( string ( body ) , "\n" )
t . Fatal ( err )
}
}
var wantResp string
if tt . wantResp != nil {
b , _ := json . Marshal ( tt . wantResp )
wantResp = string ( b )
}
}
if diff := cmp . Diff ( gotResp , tt . wantResp ) ; diff != "" {
if diff := cmp . Diff ( gotResp , string ( wantResp ) ) ; diff != "" {
t . Errorf ( "wrong response; (-got+want):%v" , diff )
t . Errorf ( "wrong response; (-got+want):%v" , diff )
}
}
// Validate cookie creation.
// Validate cookie creation.
@ -654,22 +685,13 @@ func mockLocalAPI(t *testing.T, whoIs map[string]*apitype.WhoIsResponse, self fu
t . Fatalf ( "/whois call missing \"addr\" query" )
t . Fatalf ( "/whois call missing \"addr\" query" )
}
}
if node := whoIs [ addr ] ; node != nil {
if node := whoIs [ addr ] ; node != nil {
if err := json . NewEncoder ( w ) . Encode ( & node ) ; err != nil {
writeJSON ( w , & node )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
return
return
}
}
http . Error ( w , "not a node" , http . StatusUnauthorized )
http . Error ( w , "not a node" , http . StatusUnauthorized )
return
return
case "/localapi/v0/status" :
case "/localapi/v0/status" :
status := ipnstate . Status { Self : self ( ) }
writeJSON ( w , ipnstate . Status { Self : self ( ) } )
if err := json . NewEncoder ( w ) . Encode ( status ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
return
return
case "/localapi/v0/debug-web-client" : // used by TestServeTailscaleAuth
case "/localapi/v0/debug-web-client" : // used by TestServeTailscaleAuth
type reqData struct {
type reqData struct {
@ -694,11 +716,7 @@ func mockLocalAPI(t *testing.T, whoIs map[string]*apitype.WhoIsResponse, self fu
http . Error ( w , "authenticated as wrong user" , http . StatusUnauthorized )
http . Error ( w , "authenticated as wrong user" , http . StatusUnauthorized )
return
return
}
}
if err := json . NewEncoder ( w ) . Encode ( resp ) ; err != nil {
writeJSON ( w , resp )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
return
return
default :
default :
t . Fatalf ( "unhandled localapi test endpoint %q, add to localapi handler func in test" , r . URL . Path )
t . Fatalf ( "unhandled localapi test endpoint %q, add to localapi handler func in test" , r . URL . Path )