token.go 11 KB

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