@@ -5,9 +5,11 @@ import (
55 "encoding/json"
66 "fmt"
77 "io/ioutil"
8+ "net/http"
89 "net/url"
910 "os"
1011 "os/signal"
12+ "strings"
1113 "syscall"
1214 "time"
1315
@@ -17,10 +19,12 @@ import (
1719 "github.com/cloudflare/cloudflared/logger"
1820 "github.com/cloudflare/cloudflared/origin"
1921 "github.com/coreos/go-oidc/jose"
22+ "github.com/pkg/errors"
2023)
2124
2225const (
23- keyName = "token"
26+ keyName = "token"
27+ tokenHeader = "CF_Authorization"
2428)
2529
2630type lock struct {
@@ -34,7 +38,7 @@ type signalHandler struct {
3438 signals []os.Signal
3539}
3640
37- type jwtPayload struct {
41+ type appJWTPayload struct {
3842 Aud []string `json:"aud"`
3943 Email string `json:"email"`
4044 Exp int `json:"exp"`
@@ -45,7 +49,17 @@ type jwtPayload struct {
4549 Subt string `json:"sub"`
4650}
4751
48- func (p jwtPayload ) isExpired () bool {
52+ type orgJWTPayload struct {
53+ appJWTPayload
54+ Aud string `json:"aud"`
55+ }
56+
57+ type transferServiceResponse struct {
58+ AppToken string `json:"app_token"`
59+ OrgToken string `json:"org_token"`
60+ }
61+
62+ func (p appJWTPayload ) isExpired () bool {
4963 return int (time .Now ().Unix ()) > p .Exp
5064}
5165
@@ -141,55 +155,195 @@ func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
141155
142156// getToken will either load a stored token or generate a new one
143157func getToken (appURL * url.URL , useHostOnly bool , logger logger.Service ) (string , error ) {
144- if token , err := GetTokenIfExists (appURL ); token != "" && err == nil {
158+ if token , err := GetAppTokenIfExists (appURL ); token != "" && err == nil {
145159 return token , nil
146160 }
147161
148- path , err := path .GenerateFilePathFromURL (appURL , keyName )
162+ appTokenPath , err := path .GenerateAppTokenFilePathFromURL (appURL , keyName )
149163 if err != nil {
150- return "" , err
164+ return "" , errors . Wrap ( err , "failed to generate app token file path" )
151165 }
152166
153- fileLock := newLock (path )
154-
155- err = fileLock .Acquire ()
156- if err != nil {
157- return "" , err
167+ fileLockAppToken := newLock (appTokenPath )
168+ if err = fileLockAppToken .Acquire (); err != nil {
169+ return "" , errors .Wrap (err , "failed to acquire app token lock" )
158170 }
159- defer fileLock .Release ()
171+ defer fileLockAppToken .Release ()
160172
161173 // check to see if another process has gotten a token while we waited for the lock
162- if token , err := GetTokenIfExists (appURL ); token != "" && err == nil {
174+ if token , err := GetAppTokenIfExists (appURL ); token != "" && err == nil {
163175 return token , nil
164176 }
165177
178+ // If an app token couldnt be found on disk, check for an org token and attempt to exchange it for an app token.
179+ var orgTokenPath string
180+ // Get auth domain to format into org token file path
181+ if authDomain , err := getAuthDomain (appURL ); err != nil {
182+ logger .Errorf ("failed to get auth domain: %s" , err )
183+ } else {
184+ orgToken , err := GetOrgTokenIfExists (authDomain )
185+ if err != nil {
186+ orgTokenPath , err = path .GenerateOrgTokenFilePathFromURL (authDomain )
187+ if err != nil {
188+ return "" , errors .Wrap (err , "failed to generate org token file path" )
189+ }
190+
191+ fileLockOrgToken := newLock (orgTokenPath )
192+ if err = fileLockOrgToken .Acquire (); err != nil {
193+ return "" , errors .Wrap (err , "failed to acquire org token lock" )
194+ }
195+ defer fileLockOrgToken .Release ()
196+ // check if an org token has been created since the lock was acquired
197+ orgToken , err = GetOrgTokenIfExists (authDomain )
198+ }
199+ if err == nil {
200+ if appToken , err := exchangeOrgToken (appURL , orgToken ); err != nil {
201+ logger .Debugf ("failed to exchange org token for app token: %s" , err )
202+ } else {
203+ if err := ioutil .WriteFile (appTokenPath , []byte (appToken ), 0600 ); err != nil {
204+ return "" , errors .Wrap (err , "failed to write app token to disk" )
205+ }
206+ return appToken , nil
207+ }
208+ }
209+ }
210+ return getTokensFromEdge (appURL , appTokenPath , orgTokenPath , useHostOnly , logger )
211+
212+ }
213+
214+ // getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
215+ // and return the app token.
216+ func getTokensFromEdge (appURL * url.URL , appTokenPath , orgTokenPath string , useHostOnly bool , logger logger.Service ) (string , error ) {
217+ // If no org token exists or if it couldnt be exchanged for an app token, then run the transfer service flow.
218+
166219 // this weird parameter is the resource name (token) and the key/value
167220 // we want to send to the transfer service. the key is token and the value
168221 // is blank (basically just the id generated in the transfer service)
169- token , err := transfer .Run (appURL , keyName , keyName , "" , path , true , useHostOnly , logger )
222+ resourceData , err := transfer .Run (appURL , keyName , keyName , "" , true , useHostOnly , logger )
170223 if err != nil {
171- return "" , err
224+ return "" , errors .Wrap (err , "failed to run transfer service" )
225+ }
226+ var resp transferServiceResponse
227+ if err = json .Unmarshal (resourceData , & resp ); err != nil {
228+ return "" , errors .Wrap (err , "failed to marshal transfer service response" )
229+ }
230+
231+ // If we were able to get the auth domain and generate an org token path, lets write it to disk.
232+ if orgTokenPath != "" {
233+ if err := ioutil .WriteFile (orgTokenPath , []byte (resp .OrgToken ), 0600 ); err != nil {
234+ return "" , errors .Wrap (err , "failed to write org token to disk" )
235+ }
172236 }
173237
174- return string (token ), nil
238+ if err := ioutil .WriteFile (appTokenPath , []byte (resp .AppToken ), 0600 ); err != nil {
239+ return "" , errors .Wrap (err , "failed to write app token to disk" )
240+ }
241+
242+ return resp .AppToken , nil
243+
175244}
176245
177- // GetTokenIfExists will return the token from local storage if it exists and not expired
178- func GetTokenIfExists (url * url.URL ) (string , error ) {
179- path , err := path .GenerateFilePathFromURL (url , keyName )
246+ // getAuthDomain makes a request to the appURL and stops at the first redirect. The 302 location header will contain the
247+ // auth domain
248+ func getAuthDomain (appURL * url.URL ) (string , error ) {
249+ client := & http.Client {
250+ // do not follow redirects
251+ CheckRedirect : func (req * http.Request , via []* http.Request ) error {
252+ return http .ErrUseLastResponse
253+ },
254+ Timeout : time .Second * 7 ,
255+ }
256+
257+ authDomainReq , err := http .NewRequest ("HEAD" , appURL .String (), nil )
258+ if err != nil {
259+ return "" , errors .Wrap (err , "failed to create auth domain request" )
260+ }
261+ resp , err := client .Do (authDomainReq )
262+ if err != nil {
263+ return "" , errors .Wrap (err , "failed to get auth domain" )
264+ }
265+ resp .Body .Close ()
266+ location , err := resp .Location ()
267+ if err != nil {
268+ return "" , fmt .Errorf ("failed to get auth domain. Received status code %d from %s" , resp .StatusCode , appURL .String ())
269+ }
270+ return location .Hostname (), nil
271+
272+ }
273+
274+ // exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
275+ // flow to automatically generate and return an app token without the login page.
276+ func exchangeOrgToken (appURL * url.URL , orgToken string ) (string , error ) {
277+ client := & http.Client {
278+ CheckRedirect : func (req * http.Request , via []* http.Request ) error {
279+ // attach org token to login request
280+ if strings .Contains (req .URL .Path , "cdn-cgi/access/login" ) {
281+ req .AddCookie (& http.Cookie {Name : tokenHeader , Value : orgToken })
282+ }
283+ // stop after hitting authorized endpoint since it will contain the app token
284+ if strings .Contains (via [len (via )- 1 ].URL .Path , "cdn-cgi/access/authorized" ) {
285+ return http .ErrUseLastResponse
286+ }
287+ return nil
288+ },
289+ Timeout : time .Second * 7 ,
290+ }
291+
292+ appTokenRequest , err := http .NewRequest ("HEAD" , appURL .String (), nil )
293+ if err != nil {
294+ return "" , errors .Wrap (err , "failed to create app token request" )
295+ }
296+ resp , err := client .Do (appTokenRequest )
297+ if err != nil {
298+ return "" , errors .Wrap (err , "failed to get app token" )
299+ }
300+ resp .Body .Close ()
301+ var appToken string
302+ for _ , c := range resp .Cookies () {
303+ if c .Name == tokenHeader {
304+ appToken = c .Value
305+ break
306+ }
307+ }
308+
309+ if len (appToken ) > 0 {
310+ return appToken , nil
311+ }
312+ return "" , fmt .Errorf ("response from %s did not contain app token" , resp .Request .URL .String ())
313+ }
314+
315+ func GetOrgTokenIfExists (authDomain string ) (string , error ) {
316+ path , err := path .GenerateOrgTokenFilePathFromURL (authDomain )
180317 if err != nil {
181318 return "" , err
182319 }
183- content , err := ioutil . ReadFile (path )
320+ token , err := getTokenIfExists (path )
184321 if err != nil {
185322 return "" , err
186323 }
187- token , err := jose .ParseJWT (string (content ))
324+ var payload orgJWTPayload
325+ err = json .Unmarshal (token .Payload , & payload )
188326 if err != nil {
189327 return "" , err
190328 }
191329
192- var payload jwtPayload
330+ if payload .isExpired () {
331+ err := os .Remove (path )
332+ return "" , err
333+ }
334+ return token .Encode (), nil
335+ }
336+
337+ func GetAppTokenIfExists (url * url.URL ) (string , error ) {
338+ path , err := path .GenerateAppTokenFilePathFromURL (url , keyName )
339+ if err != nil {
340+ return "" , err
341+ }
342+ token , err := getTokenIfExists (path )
343+ if err != nil {
344+ return "" , err
345+ }
346+ var payload appJWTPayload
193347 err = json .Unmarshal (token .Payload , & payload )
194348 if err != nil {
195349 return "" , err
@@ -199,13 +353,27 @@ func GetTokenIfExists(url *url.URL) (string, error) {
199353 err := os .Remove (path )
200354 return "" , err
201355 }
202-
203356 return token .Encode (), nil
357+
358+ }
359+
360+ // GetTokenIfExists will return the token from local storage if it exists and not expired
361+ func getTokenIfExists (path string ) (* jose.JWT , error ) {
362+ content , err := ioutil .ReadFile (path )
363+ if err != nil {
364+ return nil , err
365+ }
366+ token , err := jose .ParseJWT (string (content ))
367+ if err != nil {
368+ return nil , err
369+ }
370+
371+ return & token , nil
204372}
205373
206374// RemoveTokenIfExists removes the a token from local storage if it exists
207375func RemoveTokenIfExists (url * url.URL ) error {
208- path , err := path .GenerateFilePathFromURL (url , keyName )
376+ path , err := path .GenerateAppTokenFilePathFromURL (url , keyName )
209377 if err != nil {
210378 return err
211379 }
0 commit comments