client/web: move more session logic to auth.go

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy 2023-11-01 16:09:59 -04:00 committed by Sonia Appasamy
parent 535cb6c3f5
commit 7a725bb4f0
3 changed files with 58 additions and 35 deletions

View File

@ -77,8 +77,8 @@ var (
errNotOwner = errors.New("not-owner") errNotOwner = errors.New("not-owner")
) )
// getTailscaleBrowserSession retrieves the browser session associated with // getSession retrieves the browser session associated with the request,
// the request, if one exists. // if one exists.
// //
// An error is returned in any of the following cases: // An error is returned in any of the following cases:
// //
@ -104,7 +104,7 @@ var (
// //
// The WhoIsResponse is always populated, with a non-nil Node and UserProfile, // The WhoIsResponse is always populated, with a non-nil Node and UserProfile,
// unless getTailscaleBrowserSession reports errNotUsingTailscale. // unless getTailscaleBrowserSession reports errNotUsingTailscale.
func (s *Server) getTailscaleBrowserSession(r *http.Request) (*browserSession, *apitype.WhoIsResponse, error) { func (s *Server) getSession(r *http.Request) (*browserSession, *apitype.WhoIsResponse, error) {
whoIs, whoIsErr := s.lc.WhoIs(r.Context(), r.RemoteAddr) whoIs, whoIsErr := s.lc.WhoIs(r.Context(), r.RemoteAddr)
status, statusErr := s.lc.StatusWithoutPeers(r.Context()) status, statusErr := s.lc.StatusWithoutPeers(r.Context())
switch { switch {
@ -148,6 +148,52 @@ func (s *Server) getTailscaleBrowserSession(r *http.Request) (*browserSession, *
return session, whoIs, nil return session, whoIs, nil
} }
// newSession creates a new session associated with the given source user/node,
// and stores it back to the session cache. Creating of a new session includes
// generating a new auth URL from the control server.
func (s *Server) newSession(ctx context.Context, src *apitype.WhoIsResponse) (*browserSession, error) {
d, err := s.getOrAwaitAuth(ctx, "", src.Node.ID)
if err != nil {
return nil, err
}
sid, err := s.newSessionID()
if err != nil {
return nil, err
}
session := &browserSession{
ID: sid,
SrcNode: src.Node.ID,
SrcUser: src.UserProfile.ID,
AuthID: d.ID,
AuthURL: d.URL,
Created: s.timeNow(),
}
s.browserSessions.Store(sid, session)
return session, nil
}
// awaitUserAuth blocks until the given session auth has been completed
// by the user on the control server, then updates the session cache upon
// completion. An error is returned if control auth failed for any reason.
func (s *Server) awaitUserAuth(ctx context.Context, session *browserSession) error {
if session.isAuthorized(s.timeNow()) {
return nil // already authorized
}
d, err := s.getOrAwaitAuth(ctx, session.AuthID, session.SrcNode)
if err != nil {
// Clean up the session. Doing this on any error from control
// server to avoid the user getting stuck with a bad session
// cookie.
s.browserSessions.Delete(session.ID)
return err
}
if d.Complete {
session.Authenticated = d.Complete
s.browserSessions.Store(session.ID, session)
}
return nil
}
// getOrAwaitAuth connects to the control server for user auth, // getOrAwaitAuth connects to the control server for user auth,
// with the following behavior: // with the following behavior:
// //

View File

@ -214,10 +214,10 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
case strings.HasPrefix(r.URL.Path, "/api/"): case strings.HasPrefix(r.URL.Path, "/api/"):
// All other /api/ endpoints require a valid browser session. // All other /api/ endpoints require a valid browser session.
// //
// TODO(sonia): s.getTailscaleBrowserSession calls whois again, // TODO(sonia): s.getSession calls whois again,
// should try and use the above call instead of running another // should try and use the above call instead of running another
// localapi request. // localapi request.
session, _, err := s.getTailscaleBrowserSession(r) session, _, err := s.getSession(r)
if err != nil || !session.isAuthorized(s.timeNow()) { if err != nil || !session.isAuthorized(s.timeNow()) {
http.Error(w, "no valid session", http.StatusUnauthorized) http.Error(w, "no valid session", http.StatusUnauthorized)
return false return false
@ -289,57 +289,34 @@ func (s *Server) serveTailscaleAuth(w http.ResponseWriter, r *http.Request) {
} }
var resp authResponse var resp authResponse
session, whois, err := s.getTailscaleBrowserSession(r) session, whois, err := s.getSession(r)
switch { switch {
case err != nil && !errors.Is(err, errNoSession): case err != nil && !errors.Is(err, errNoSession):
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
case session == nil: case session == nil:
// Create a new session. // Create a new session.
d, err := s.getOrAwaitAuth(r.Context(), "", whois.Node.ID) session, err := s.newSession(r.Context(), whois)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
sid, err := s.newSessionID()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session := &browserSession{
ID: sid,
SrcNode: whois.Node.ID,
SrcUser: whois.UserProfile.ID,
AuthID: d.ID,
AuthURL: d.URL,
Created: s.timeNow(),
}
s.browserSessions.Store(sid, session)
// Set the cookie on browser. // Set the cookie on browser.
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: sessionCookieName, Name: sessionCookieName,
Value: sid, Value: session.ID,
Raw: sid, Raw: session.ID,
Path: "/", Path: "/",
Expires: session.expires(), Expires: session.expires(),
}) })
resp = authResponse{OK: false, AuthURL: d.URL} resp = authResponse{OK: false, AuthURL: session.AuthURL}
case !session.isAuthorized(s.timeNow()): case !session.isAuthorized(s.timeNow()):
if r.URL.Query().Get("wait") == "true" { if r.URL.Query().Get("wait") == "true" {
// Client requested we block until user completes auth. // Client requested we block until user completes auth.
d, err := s.getOrAwaitAuth(r.Context(), session.AuthID, whois.Node.ID) if err := s.awaitUserAuth(r.Context(), session); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
// Clean up the session. Doing this on any error from control
// server to avoid the user getting stuck with a bad session
// cookie.
s.browserSessions.Delete(session.ID)
return return
} }
if d.Complete {
session.Authenticated = d.Complete
s.browserSessions.Store(session.ID, session)
}
} }
if session.isAuthorized(s.timeNow()) { if session.isAuthorized(s.timeNow()) {
resp = authResponse{OK: true} resp = authResponse{OK: true}

View File

@ -302,7 +302,7 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
if tt.cookie != "" { if tt.cookie != "" {
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tt.cookie}) r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tt.cookie})
} }
session, _, err := s.getTailscaleBrowserSession(r) session, _, err := s.getSession(r)
if !errors.Is(err, tt.wantError) { if !errors.Is(err, tt.wantError) {
t.Errorf("wrong error; want=%v, got=%v", tt.wantError, err) t.Errorf("wrong error; want=%v, got=%v", tt.wantError, err)
} }