token.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. package token
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "os/signal"
  10. "strings"
  11. "syscall"
  12. "time"
  13. "github.com/go-jose/go-jose/v4"
  14. "github.com/pkg/errors"
  15. "github.com/rs/zerolog"
  16. "github.com/cloudflare/cloudflared/config"
  17. "github.com/cloudflare/cloudflared/retry"
  18. )
  19. const (
  20. keyName = "token"
  21. tokenCookie = "CF_Authorization"
  22. appSessionCookie = "CF_AppSession"
  23. appDomainHeader = "CF-Access-Domain"
  24. appAUDHeader = "CF-Access-Aud"
  25. AccessLoginWorkerPath = "/cdn-cgi/access/login"
  26. AccessAuthorizedWorkerPath = "/cdn-cgi/access/authorized"
  27. )
  28. var (
  29. userAgent = "DEV"
  30. signatureAlgs = []jose.SignatureAlgorithm{jose.RS256}
  31. )
  32. type AppInfo struct {
  33. AuthDomain string
  34. AppAUD string
  35. AppDomain string
  36. }
  37. type lock struct {
  38. lockFilePath string
  39. backoff *retry.BackoffHandler
  40. sigHandler *signalHandler
  41. }
  42. type signalHandler struct {
  43. sigChannel chan os.Signal
  44. signals []os.Signal
  45. }
  46. type jwtPayload struct {
  47. Aud []string `json:"aud"`
  48. Email string `json:"email"`
  49. Exp int `json:"exp"`
  50. Iat int `json:"iat"`
  51. Nbf int `json:"nbf"`
  52. Iss string `json:"iss"`
  53. Type string `json:"type"`
  54. Subt string `json:"sub"`
  55. }
  56. type transferServiceResponse struct {
  57. AppToken string `json:"app_token"`
  58. OrgToken string `json:"org_token"`
  59. }
  60. func (p jwtPayload) isExpired() bool {
  61. return int(time.Now().Unix()) > p.Exp
  62. }
  63. func (s *signalHandler) register(handler func()) {
  64. s.sigChannel = make(chan os.Signal, 1)
  65. signal.Notify(s.sigChannel, s.signals...)
  66. go func(s *signalHandler) {
  67. for range s.sigChannel {
  68. handler()
  69. }
  70. }(s)
  71. }
  72. func (s *signalHandler) deregister() {
  73. signal.Stop(s.sigChannel)
  74. close(s.sigChannel)
  75. }
  76. func errDeleteTokenFailed(lockFilePath string) error {
  77. return fmt.Errorf("failed to acquire a new Access token. Please try to delete %s", lockFilePath)
  78. }
  79. // newLock will get a new file lock
  80. func newLock(path string) *lock {
  81. lockPath := path + ".lock"
  82. backoff := retry.NewBackoff(uint(7), retry.DefaultBaseTime, false)
  83. return &lock{
  84. lockFilePath: lockPath,
  85. backoff: &backoff,
  86. sigHandler: &signalHandler{
  87. signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
  88. },
  89. }
  90. }
  91. func (l *lock) Acquire() error {
  92. // Intercept SIGINT and SIGTERM to release lock before exiting
  93. l.sigHandler.register(func() {
  94. _ = l.deleteLockFile()
  95. os.Exit(0)
  96. })
  97. // Check for a lock file
  98. // if the lock file exists; start polling
  99. // if not, create the lock file and go through the normal flow.
  100. // See AUTH-1736 for the reason why we do all this
  101. for isTokenLocked(l.lockFilePath) {
  102. if l.backoff.Backoff(context.Background()) {
  103. continue
  104. }
  105. if err := l.deleteLockFile(); err != nil {
  106. return err
  107. }
  108. }
  109. // Create a lock file so other processes won't also try to get the token at
  110. // the same time
  111. if err := os.WriteFile(l.lockFilePath, []byte{}, 0600); err != nil {
  112. return err
  113. }
  114. return nil
  115. }
  116. func (l *lock) deleteLockFile() error {
  117. if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
  118. return errDeleteTokenFailed(l.lockFilePath)
  119. }
  120. return nil
  121. }
  122. func (l *lock) Release() error {
  123. defer l.sigHandler.deregister()
  124. return l.deleteLockFile()
  125. }
  126. // isTokenLocked checks to see if there is another process attempting to get the token already
  127. func isTokenLocked(lockFilePath string) bool {
  128. exists, err := config.FileExists(lockFilePath)
  129. return exists && err == nil
  130. }
  131. func Init(version string) {
  132. userAgent = fmt.Sprintf("cloudflared/%s", version)
  133. }
  134. // FetchTokenWithRedirect will either load a stored token or generate a new one
  135. // it appends the full url as the redirect URL to the access cli request if opening the browser
  136. func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) {
  137. return getToken(appURL, appInfo, false, log)
  138. }
  139. // FetchToken will either load a stored token or generate a new one
  140. // it appends the host of the appURL as the redirect URL to the access cli request if opening the browser
  141. func FetchToken(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) {
  142. return getToken(appURL, appInfo, true, log)
  143. }
  144. // getToken will either load a stored token or generate a new one
  145. func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.Logger) (string, error) {
  146. if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil {
  147. return token, nil
  148. }
  149. appTokenPath, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
  150. if err != nil {
  151. return "", errors.Wrap(err, "failed to generate app token file path")
  152. }
  153. fileLockAppToken := newLock(appTokenPath)
  154. if err = fileLockAppToken.Acquire(); err != nil {
  155. return "", errors.Wrap(err, "failed to acquire app token lock")
  156. }
  157. defer fileLockAppToken.Release()
  158. // check to see if another process has gotten a token while we waited for the lock
  159. if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil {
  160. return token, nil
  161. }
  162. // If an app token couldn't be found on disk, check for an org token and attempt to exchange it for an app token.
  163. var orgTokenPath string
  164. orgToken, err := GetOrgTokenIfExists(appInfo.AuthDomain)
  165. if err != nil {
  166. orgTokenPath, err = generateOrgTokenFilePathFromURL(appInfo.AuthDomain)
  167. if err != nil {
  168. return "", errors.Wrap(err, "failed to generate org token file path")
  169. }
  170. fileLockOrgToken := newLock(orgTokenPath)
  171. if err = fileLockOrgToken.Acquire(); err != nil {
  172. return "", errors.Wrap(err, "failed to acquire org token lock")
  173. }
  174. defer fileLockOrgToken.Release()
  175. // check if an org token has been created since the lock was acquired
  176. orgToken, err = GetOrgTokenIfExists(appInfo.AuthDomain)
  177. }
  178. if err == nil {
  179. if appToken, err := exchangeOrgToken(appURL, orgToken); err != nil {
  180. log.Debug().Msgf("failed to exchange org token for app token: %s", err)
  181. } else {
  182. // generate app path
  183. if err := os.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil {
  184. return "", errors.Wrap(err, "failed to write app token to disk")
  185. }
  186. return appToken, nil
  187. }
  188. }
  189. return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, log)
  190. }
  191. // getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
  192. // and return the app token.
  193. func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, log *zerolog.Logger) (string, error) {
  194. // If no org token exists or if it couldn't be exchanged for an app token, then run the transfer service flow.
  195. // this weird parameter is the resource name (token) and the key/value
  196. // we want to send to the transfer service. the key is token and the value
  197. // is blank (basically just the id generated in the transfer service)
  198. resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, log)
  199. if err != nil {
  200. return "", errors.Wrap(err, "failed to run transfer service")
  201. }
  202. var resp transferServiceResponse
  203. if err = json.Unmarshal(resourceData, &resp); err != nil {
  204. return "", errors.Wrap(err, "failed to marshal transfer service response")
  205. }
  206. // If we were able to get the auth domain and generate an org token path, lets write it to disk.
  207. if orgTokenPath != "" {
  208. if err := os.WriteFile(orgTokenPath, []byte(resp.OrgToken), 0600); err != nil {
  209. return "", errors.Wrap(err, "failed to write org token to disk")
  210. }
  211. }
  212. if err := os.WriteFile(appTokenPath, []byte(resp.AppToken), 0600); err != nil {
  213. return "", errors.Wrap(err, "failed to write app token to disk")
  214. }
  215. return resp.AppToken, nil
  216. }
  217. // GetAppInfo makes a request to the appURL and stops at the first redirect. The 302 location header will contain the
  218. // auth domain
  219. func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
  220. client := &http.Client{
  221. // do not follow redirects
  222. CheckRedirect: func(req *http.Request, via []*http.Request) error {
  223. // stop after hitting login endpoint since it will contain app path
  224. if strings.Contains(via[len(via)-1].URL.Path, AccessLoginWorkerPath) {
  225. return http.ErrUseLastResponse
  226. }
  227. return nil
  228. },
  229. Timeout: time.Second * 7,
  230. }
  231. appInfoReq, err := http.NewRequest("HEAD", reqURL.String(), nil)
  232. if err != nil {
  233. return nil, errors.Wrap(err, "failed to create app info request")
  234. }
  235. appInfoReq.Header.Add("User-Agent", userAgent)
  236. resp, err := client.Do(appInfoReq)
  237. if err != nil {
  238. return nil, errors.Wrap(err, "failed to get app info")
  239. }
  240. resp.Body.Close()
  241. var aud string
  242. location := resp.Request.URL
  243. if strings.Contains(location.Path, AccessLoginWorkerPath) {
  244. aud = resp.Request.URL.Query().Get("kid")
  245. if aud == "" {
  246. return nil, errors.New("Empty app aud")
  247. }
  248. } else if audHeader := resp.Header.Get(appAUDHeader); audHeader != "" {
  249. // 403/401 from the edge will have aud in a header
  250. aud = audHeader
  251. } else {
  252. return nil, fmt.Errorf("failed to find Access application at %s", reqURL.String())
  253. }
  254. domain := resp.Header.Get(appDomainHeader)
  255. if domain == "" {
  256. return nil, errors.New("Empty app domain")
  257. }
  258. return &AppInfo{location.Hostname(), aud, domain}, nil
  259. }
  260. func handleRedirects(req *http.Request, via []*http.Request, orgToken string) error {
  261. // attach org token to login request
  262. if strings.Contains(req.URL.Path, AccessLoginWorkerPath) {
  263. req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken})
  264. }
  265. // attach app session cookie to authorized request
  266. if strings.Contains(req.URL.Path, AccessAuthorizedWorkerPath) {
  267. // We need to check and see if the CF_APP_SESSION cookie was set
  268. for _, prevReq := range via {
  269. if prevReq != nil && prevReq.Response != nil {
  270. for _, c := range prevReq.Response.Cookies() {
  271. if c.Name == appSessionCookie {
  272. req.AddCookie(&http.Cookie{Name: appSessionCookie, Value: c.Value})
  273. return nil
  274. }
  275. }
  276. }
  277. }
  278. }
  279. // stop after hitting authorized endpoint since it will contain the app token
  280. if len(via) > 0 && strings.Contains(via[len(via)-1].URL.Path, AccessAuthorizedWorkerPath) {
  281. return http.ErrUseLastResponse
  282. }
  283. return nil
  284. }
  285. // exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
  286. // flow to automatically generate and return an app token without the login page.
  287. func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
  288. client := &http.Client{
  289. CheckRedirect: func(req *http.Request, via []*http.Request) error {
  290. return handleRedirects(req, via, orgToken)
  291. },
  292. Timeout: time.Second * 7,
  293. }
  294. appTokenRequest, err := http.NewRequest("HEAD", appURL.String(), nil)
  295. if err != nil {
  296. return "", errors.Wrap(err, "failed to create app token request")
  297. }
  298. appTokenRequest.Header.Add("User-Agent", userAgent)
  299. resp, err := client.Do(appTokenRequest)
  300. if err != nil {
  301. return "", errors.Wrap(err, "failed to get app token")
  302. }
  303. resp.Body.Close()
  304. var appToken string
  305. for _, c := range resp.Cookies() {
  306. //if Org token revoked on exchange, getTokensFromEdge instead
  307. validAppToken := c.Name == tokenCookie && time.Now().Before(c.Expires)
  308. if validAppToken {
  309. appToken = c.Value
  310. break
  311. }
  312. }
  313. if len(appToken) > 0 {
  314. return appToken, nil
  315. }
  316. return "", fmt.Errorf("response from %s did not contain app token", resp.Request.URL.String())
  317. }
  318. func GetOrgTokenIfExists(authDomain string) (string, error) {
  319. path, err := generateOrgTokenFilePathFromURL(authDomain)
  320. if err != nil {
  321. return "", err
  322. }
  323. token, err := getTokenIfExists(path)
  324. if err != nil {
  325. return "", err
  326. }
  327. var payload jwtPayload
  328. err = json.Unmarshal(token.UnsafePayloadWithoutVerification(), &payload)
  329. if err != nil {
  330. return "", err
  331. }
  332. if payload.isExpired() {
  333. err := os.Remove(path)
  334. return "", err
  335. }
  336. return token.CompactSerialize()
  337. }
  338. func GetAppTokenIfExists(appInfo *AppInfo) (string, error) {
  339. path, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
  340. if err != nil {
  341. return "", err
  342. }
  343. token, err := getTokenIfExists(path)
  344. if err != nil {
  345. return "", err
  346. }
  347. var payload jwtPayload
  348. err = json.Unmarshal(token.UnsafePayloadWithoutVerification(), &payload)
  349. if err != nil {
  350. return "", err
  351. }
  352. if payload.isExpired() {
  353. err := os.Remove(path)
  354. return "", err
  355. }
  356. return token.CompactSerialize()
  357. }
  358. // GetTokenIfExists will return the token from local storage if it exists and not expired
  359. func getTokenIfExists(path string) (*jose.JSONWebSignature, error) {
  360. content, err := os.ReadFile(path)
  361. if err != nil {
  362. return nil, err
  363. }
  364. token, err := jose.ParseSigned(string(content), signatureAlgs)
  365. if err != nil {
  366. return nil, err
  367. }
  368. return token, nil
  369. }
  370. // RemoveTokenIfExists removes the a token from local storage if it exists
  371. func RemoveTokenIfExists(appInfo *AppInfo) error {
  372. path, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
  373. if err != nil {
  374. return err
  375. }
  376. if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
  377. return err
  378. }
  379. return nil
  380. }