token.go 13 KB

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