Skip to content

Commit fcc393e

Browse files
committed
AUTH-3221: Saves org token to disk and uses it to refresh the app token
1 parent cad58b9 commit fcc393e

File tree

7 files changed

+229
-44
lines changed

7 files changed

+229
-44
lines changed

cmd/cloudflared/access/cmd.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func login(c *cli.Context) error {
222222
return err
223223
}
224224

225-
cfdToken, err := token.GetTokenIfExists(appURL)
225+
cfdToken, err := token.GetAppTokenIfExists(appURL)
226226
if err != nil {
227227
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
228228
return err
@@ -267,7 +267,7 @@ func curl(c *cli.Context) error {
267267
return err
268268
}
269269

270-
tok, err := token.GetTokenIfExists(appURL)
270+
tok, err := token.GetAppTokenIfExists(appURL)
271271
if err != nil || tok == "" {
272272
if allowRequest {
273273
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.")
@@ -295,7 +295,7 @@ func generateToken(c *cli.Context) error {
295295
fmt.Fprintln(os.Stderr, "Please provide a url.")
296296
return err
297297
}
298-
tok, err := token.GetTokenIfExists(appURL)
298+
tok, err := token.GetAppTokenIfExists(appURL)
299299
if err != nil || tok == "" {
300300
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
301301
return err

cmd/cloudflared/path/path.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,27 @@ import (
1111
"github.com/mitchellh/go-homedir"
1212
)
1313

14-
// GenerateFilePathFromURL will return a filepath for given access application url
15-
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
14+
// GenerateAppTokenFilePathFromURL will return a filepath for given Access org token
15+
func GenerateAppTokenFilePathFromURL(url *url.URL, suffix string) (string, error) {
16+
configPath, err := getConfigPath()
17+
if err != nil {
18+
return "", err
19+
}
20+
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
21+
return filepath.Join(configPath, name), nil
22+
}
23+
24+
// GenerateOrgTokenFilePathFromURL will return a filepath for given Access application token
25+
func GenerateOrgTokenFilePathFromURL(authDomain string) (string, error) {
26+
configPath, err := getConfigPath()
27+
if err != nil {
28+
return "", err
29+
}
30+
name := strings.Replace(fmt.Sprintf("%s-org-token", authDomain), "/", "-", -1)
31+
return filepath.Join(configPath, name), nil
32+
}
33+
34+
func getConfigPath() (string, error) {
1635
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
1736
if err != nil {
1837
return "", err
@@ -22,9 +41,5 @@ func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
2241
// create config directory if doesn't already exist
2342
err = os.Mkdir(configPath, 0700)
2443
}
25-
if err != nil {
26-
return "", err
27-
}
28-
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
29-
return filepath.Join(configPath, name), nil
44+
return configPath, err
3045
}

cmd/cloudflared/token/token.go

Lines changed: 192 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2225
const (
23-
keyName = "token"
26+
keyName = "token"
27+
tokenHeader = "CF_Authorization"
2428
)
2529

2630
type 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
143157
func 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
207375
func 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

Comments
 (0)