auth.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package web
  4. import (
  5. "context"
  6. "crypto/rand"
  7. "encoding/base64"
  8. "errors"
  9. "fmt"
  10. "net/http"
  11. "net/url"
  12. "strings"
  13. "time"
  14. "tailscale.com/client/tailscale/apitype"
  15. "tailscale.com/ipn/ipnstate"
  16. "tailscale.com/tailcfg"
  17. )
  18. const (
  19. sessionCookieName = "TS-Web-Session"
  20. sessionCookieExpiry = time.Hour * 24 * 30 // 30 days
  21. )
  22. // browserSession holds data about a user's browser session
  23. // on the full management web client.
  24. type browserSession struct {
  25. // ID is the unique identifier for the session.
  26. // It is passed in the user's "TS-Web-Session" browser cookie.
  27. ID string
  28. SrcNode tailcfg.NodeID
  29. SrcUser tailcfg.UserID
  30. AuthID string // from tailcfg.WebClientAuthResponse
  31. AuthURL string // from tailcfg.WebClientAuthResponse
  32. Created time.Time
  33. Authenticated bool
  34. }
  35. // isAuthorized reports true if the given session is authorized
  36. // to be used by its associated user to access the full management
  37. // web client.
  38. //
  39. // isAuthorized is true only when s.Authenticated is true (i.e.
  40. // the user has authenticated the session) and the session is not
  41. // expired.
  42. // 2023-10-05: Sessions expire by default 30 days after creation.
  43. func (s *browserSession) isAuthorized(now time.Time) bool {
  44. switch {
  45. case s == nil:
  46. return false
  47. case !s.Authenticated:
  48. return false // awaiting auth
  49. case s.isExpired(now):
  50. return false // expired
  51. }
  52. return true
  53. }
  54. // isExpired reports true if s is expired.
  55. // 2023-10-05: Sessions expire by default 30 days after creation.
  56. func (s *browserSession) isExpired(now time.Time) bool {
  57. return !s.Created.IsZero() && now.After(s.expires())
  58. }
  59. // expires reports when the given session expires.
  60. func (s *browserSession) expires() time.Time {
  61. return s.Created.Add(sessionCookieExpiry)
  62. }
  63. var (
  64. errNoSession = errors.New("no-browser-session")
  65. errNotUsingTailscale = errors.New("not-using-tailscale")
  66. errTaggedRemoteSource = errors.New("tagged-remote-source")
  67. errTaggedLocalSource = errors.New("tagged-local-source")
  68. errNotOwner = errors.New("not-owner")
  69. )
  70. // getSession retrieves the browser session associated with the request,
  71. // if one exists.
  72. //
  73. // An error is returned in any of the following cases:
  74. //
  75. // - (errNotUsingTailscale) The request was not made over tailscale.
  76. //
  77. // - (errNoSession) The request does not have a session.
  78. //
  79. // - (errTaggedRemoteSource) The source is remote (another node) and tagged.
  80. // Users must use their own user-owned devices to manage other nodes'
  81. // web clients.
  82. //
  83. // - (errTaggedLocalSource) The source is local (the same node) and tagged.
  84. // Tagged nodes can only be remotely managed, allowing ACLs to dictate
  85. // access to web clients.
  86. //
  87. // - (errNotOwner) The source is not the owner of this client (if the
  88. // client is user-owned). Only the owner is allowed to manage the
  89. // node via the web client.
  90. //
  91. // If no error is returned, the browserSession is always non-nil.
  92. // getTailscaleBrowserSession does not check whether the session has been
  93. // authorized by the user. Callers can use browserSession.isAuthorized.
  94. //
  95. // The WhoIsResponse is always populated, with a non-nil Node and UserProfile,
  96. // unless getTailscaleBrowserSession reports errNotUsingTailscale.
  97. func (s *Server) getSession(r *http.Request) (*browserSession, *apitype.WhoIsResponse, *ipnstate.Status, error) {
  98. whoIs, whoIsErr := s.lc.WhoIs(r.Context(), r.RemoteAddr)
  99. status, statusErr := s.lc.StatusWithoutPeers(r.Context())
  100. switch {
  101. case whoIsErr != nil:
  102. return nil, nil, status, errNotUsingTailscale
  103. case statusErr != nil:
  104. return nil, whoIs, nil, statusErr
  105. case status.Self == nil:
  106. return nil, whoIs, status, errors.New("missing self node in tailscale status")
  107. case whoIs.Node.IsTagged() && whoIs.Node.StableID == status.Self.ID:
  108. return nil, whoIs, status, errTaggedLocalSource
  109. case whoIs.Node.IsTagged():
  110. return nil, whoIs, status, errTaggedRemoteSource
  111. case !status.Self.IsTagged() && status.Self.UserID != whoIs.UserProfile.ID:
  112. return nil, whoIs, status, errNotOwner
  113. }
  114. srcNode := whoIs.Node.ID
  115. srcUser := whoIs.UserProfile.ID
  116. cookie, err := r.Cookie(sessionCookieName)
  117. if errors.Is(err, http.ErrNoCookie) {
  118. return nil, whoIs, status, errNoSession
  119. } else if err != nil {
  120. return nil, whoIs, status, err
  121. }
  122. v, ok := s.browserSessions.Load(cookie.Value)
  123. if !ok {
  124. return nil, whoIs, status, errNoSession
  125. }
  126. session := v.(*browserSession)
  127. if session.SrcNode != srcNode || session.SrcUser != srcUser {
  128. // In this case the browser cookie is associated with another tailscale node.
  129. // Maybe the source browser's machine was logged out and then back in as a different node.
  130. // Return errNoSession because there is no session for this user.
  131. return nil, whoIs, status, errNoSession
  132. } else if session.isExpired(s.timeNow()) {
  133. // Session expired, remove from session map and return errNoSession.
  134. s.browserSessions.Delete(session.ID)
  135. return nil, whoIs, status, errNoSession
  136. }
  137. return session, whoIs, status, nil
  138. }
  139. // newSession creates a new session associated with the given source user/node,
  140. // and stores it back to the session cache. Creating of a new session includes
  141. // generating a new auth URL from the control server.
  142. func (s *Server) newSession(ctx context.Context, src *apitype.WhoIsResponse) (*browserSession, error) {
  143. sid, err := s.newSessionID()
  144. if err != nil {
  145. return nil, err
  146. }
  147. session := &browserSession{
  148. ID: sid,
  149. SrcNode: src.Node.ID,
  150. SrcUser: src.UserProfile.ID,
  151. Created: s.timeNow(),
  152. }
  153. if s.controlSupportsCheckMode(ctx) {
  154. // control supports check mode, so get a new auth URL and return.
  155. a, err := s.newAuthURL(ctx, src.Node.ID)
  156. if err != nil {
  157. return nil, err
  158. }
  159. session.AuthID = a.ID
  160. session.AuthURL = a.URL
  161. } else {
  162. // control does not support check mode, so there is no additional auth we can do.
  163. session.Authenticated = true
  164. }
  165. s.browserSessions.Store(sid, session)
  166. return session, nil
  167. }
  168. // controlSupportsCheckMode returns whether the current control server supports web client check mode, to verify a user's identity.
  169. // We assume that only "tailscale.com" control servers support check mode.
  170. // This allows the web client to be used with non-standard control servers.
  171. // If an error occurs getting the control URL, this method returns true to fail closed.
  172. //
  173. // TODO(juanfont/headscale#1623): adjust or remove this when headscale supports check mode.
  174. func (s *Server) controlSupportsCheckMode(ctx context.Context) bool {
  175. prefs, err := s.lc.GetPrefs(ctx)
  176. if err != nil {
  177. return true
  178. }
  179. controlURL, err := url.Parse(prefs.ControlURLOrDefault())
  180. if err != nil {
  181. return true
  182. }
  183. return strings.HasSuffix(controlURL.Host, ".tailscale.com")
  184. }
  185. // awaitUserAuth blocks until the given session auth has been completed
  186. // by the user on the control server, then updates the session cache upon
  187. // completion. An error is returned if control auth failed for any reason.
  188. func (s *Server) awaitUserAuth(ctx context.Context, session *browserSession) error {
  189. if session.isAuthorized(s.timeNow()) {
  190. return nil // already authorized
  191. }
  192. a, err := s.waitAuthURL(ctx, session.AuthID, session.SrcNode)
  193. if err != nil {
  194. // Clean up the session. Doing this on any error from control
  195. // server to avoid the user getting stuck with a bad session
  196. // cookie.
  197. s.browserSessions.Delete(session.ID)
  198. return err
  199. }
  200. if a.Complete {
  201. session.Authenticated = a.Complete
  202. s.browserSessions.Store(session.ID, session)
  203. }
  204. return nil
  205. }
  206. func (s *Server) newSessionID() (string, error) {
  207. raw := make([]byte, 16)
  208. for i := 0; i < 5; i++ {
  209. if _, err := rand.Read(raw); err != nil {
  210. return "", err
  211. }
  212. cookie := "ts-web-" + base64.RawURLEncoding.EncodeToString(raw)
  213. if _, ok := s.browserSessions.Load(cookie); !ok {
  214. return cookie, nil
  215. }
  216. }
  217. return "", errors.New("too many collisions generating new session; please refresh page")
  218. }
  219. type peerCapabilities map[capFeature]bool // value is true if the peer can edit the given feature
  220. // canEdit is true if the peerCapabilities grant edit access
  221. // to the given feature.
  222. func (p peerCapabilities) canEdit(feature capFeature) bool {
  223. if p == nil {
  224. return false
  225. }
  226. if p[capFeatureAll] {
  227. return true
  228. }
  229. return p[feature]
  230. }
  231. type capFeature string
  232. const (
  233. // The following values should not be edited.
  234. // New caps can be added, but existing ones should not be changed,
  235. // as these exact values are used by users in tailnet policy files.
  236. capFeatureAll capFeature = "*" // grants peer management of all features
  237. capFeatureFunnel capFeature = "funnel" // grants peer serve/funnel management
  238. capFeatureSSH capFeature = "ssh" // grants peer SSH server management
  239. capFeatureSubnet capFeature = "subnet" // grants peer subnet routes management
  240. capFeatureExitNode capFeature = "exitnode" // grants peer ability to advertise-as and use exit nodes
  241. capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
  242. )
  243. type capRule struct {
  244. CanEdit []string `json:"canEdit,omitempty"` // list of features peer is allowed to edit
  245. }
  246. // toPeerCapabilities parses out the web ui capabilities from the
  247. // given whois response.
  248. func toPeerCapabilities(whois *apitype.WhoIsResponse) (peerCapabilities, error) {
  249. caps := peerCapabilities{}
  250. if whois == nil {
  251. return caps, nil
  252. }
  253. rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI)
  254. if err != nil {
  255. return nil, fmt.Errorf("failed to unmarshal capability: %v", err)
  256. }
  257. for _, c := range rules {
  258. for _, f := range c.CanEdit {
  259. caps[capFeature(strings.ToLower(f))] = true
  260. }
  261. }
  262. return caps, nil
  263. }