AUTH-3221: Saves org token to disk and uses it to refresh the app token

This commit is contained in:
Michael Borkenstein 2020-11-08 21:25:35 -06:00
parent cad58b9b57
commit fcc393e2f0
7 changed files with 237 additions and 52 deletions

View File

@ -222,7 +222,7 @@ func login(c *cli.Context) error {
return err return err
} }
cfdToken, err := token.GetTokenIfExists(appURL) cfdToken, err := token.GetAppTokenIfExists(appURL)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.") fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
return err return err
@ -267,7 +267,7 @@ func curl(c *cli.Context) error {
return err return err
} }
tok, err := token.GetTokenIfExists(appURL) tok, err := token.GetAppTokenIfExists(appURL)
if err != nil || tok == "" { if err != nil || tok == "" {
if allowRequest { if allowRequest {
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.") 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 {
fmt.Fprintln(os.Stderr, "Please provide a url.") fmt.Fprintln(os.Stderr, "Please provide a url.")
return err return err
} }
tok, err := token.GetTokenIfExists(appURL) tok, err := token.GetAppTokenIfExists(appURL)
if err != nil || tok == "" { if err != nil || tok == "" {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.") fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
return err return err

View File

@ -11,8 +11,27 @@ import (
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
) )
// GenerateFilePathFromURL will return a filepath for given access application url // GenerateAppTokenFilePathFromURL will return a filepath for given Access org token
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) { func GenerateAppTokenFilePathFromURL(url *url.URL, suffix string) (string, error) {
configPath, err := getConfigPath()
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
return filepath.Join(configPath, name), nil
}
// GenerateOrgTokenFilePathFromURL will return a filepath for given Access application token
func GenerateOrgTokenFilePathFromURL(authDomain string) (string, error) {
configPath, err := getConfigPath()
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s-org-token", authDomain), "/", "-", -1)
return filepath.Join(configPath, name), nil
}
func getConfigPath() (string, error) {
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0]) configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
if err != nil { if err != nil {
return "", err return "", err
@ -22,9 +41,5 @@ func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
// create config directory if doesn't already exist // create config directory if doesn't already exist
err = os.Mkdir(configPath, 0700) err = os.Mkdir(configPath, 0700)
} }
if err != nil { return configPath, err
return "", err
}
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
return filepath.Join(configPath, name), nil
} }

View File

@ -5,9 +5,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
@ -17,10 +19,12 @@ import (
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/origin"
"github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/jose"
"github.com/pkg/errors"
) )
const ( const (
keyName = "token" keyName = "token"
tokenHeader = "CF_Authorization"
) )
type lock struct { type lock struct {
@ -34,7 +38,7 @@ type signalHandler struct {
signals []os.Signal signals []os.Signal
} }
type jwtPayload struct { type appJWTPayload struct {
Aud []string `json:"aud"` Aud []string `json:"aud"`
Email string `json:"email"` Email string `json:"email"`
Exp int `json:"exp"` Exp int `json:"exp"`
@ -45,7 +49,17 @@ type jwtPayload struct {
Subt string `json:"sub"` Subt string `json:"sub"`
} }
func (p jwtPayload) isExpired() bool { type orgJWTPayload struct {
appJWTPayload
Aud string `json:"aud"`
}
type transferServiceResponse struct {
AppToken string `json:"app_token"`
OrgToken string `json:"org_token"`
}
func (p appJWTPayload) isExpired() bool {
return int(time.Now().Unix()) > p.Exp return int(time.Now().Unix()) > p.Exp
} }
@ -141,55 +155,173 @@ func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
// getToken will either load a stored token or generate a new one // getToken will either load a stored token or generate a new one
func getToken(appURL *url.URL, useHostOnly bool, logger logger.Service) (string, error) { func getToken(appURL *url.URL, useHostOnly bool, logger logger.Service) (string, error) {
if token, err := GetTokenIfExists(appURL); token != "" && err == nil { if token, err := GetAppTokenIfExists(appURL); token != "" && err == nil {
return token, nil return token, nil
} }
path, err := path.GenerateFilePathFromURL(appURL, keyName) appTokenPath, err := path.GenerateAppTokenFilePathFromURL(appURL, keyName)
if err != nil { if err != nil {
return "", err return "", errors.Wrap(err, "failed to generate app token file path")
} }
fileLock := newLock(path) fileLockAppToken := newLock(appTokenPath)
if err = fileLockAppToken.Acquire(); err != nil {
err = fileLock.Acquire() return "", errors.Wrap(err, "failed to acquire app token lock")
if err != nil {
return "", err
} }
defer fileLock.Release() defer fileLockAppToken.Release()
// check to see if another process has gotten a token while we waited for the lock // check to see if another process has gotten a token while we waited for the lock
if token, err := GetTokenIfExists(appURL); token != "" && err == nil { if token, err := GetAppTokenIfExists(appURL); token != "" && err == nil {
return token, nil return token, nil
} }
// If an app token couldnt be found on disk, check for an org token and attempt to exchange it for an app token.
var orgTokenPath string
// Get auth domain to format into org token file path
if authDomain, err := getAuthDomain(appURL); err != nil {
logger.Errorf("failed to get auth domain: %s", err)
} else {
orgToken, err := GetOrgTokenIfExists(authDomain)
if err != nil {
orgTokenPath, err = path.GenerateOrgTokenFilePathFromURL(authDomain)
if err != nil {
return "", errors.Wrap(err, "failed to generate org token file path")
}
fileLockOrgToken := newLock(orgTokenPath)
if err = fileLockOrgToken.Acquire(); err != nil {
return "", errors.Wrap(err, "failed to acquire org token lock")
}
defer fileLockOrgToken.Release()
// check if an org token has been created since the lock was acquired
orgToken, err = GetOrgTokenIfExists(authDomain)
}
if err == nil {
if appToken, err := exchangeOrgToken(appURL, orgToken); err != nil {
logger.Debugf("failed to exchange org token for app token: %s", err)
} else {
if err := ioutil.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil {
return "", errors.Wrap(err, "failed to write app token to disk")
}
return appToken, nil
}
}
}
return getTokensFromEdge(appURL, appTokenPath, orgTokenPath, useHostOnly, logger)
}
// getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
// and return the app token.
func getTokensFromEdge(appURL *url.URL, appTokenPath, orgTokenPath string, useHostOnly bool, logger logger.Service) (string, error) {
// If no org token exists or if it couldnt be exchanged for an app token, then run the transfer service flow.
// this weird parameter is the resource name (token) and the key/value // this weird parameter is the resource name (token) and the key/value
// we want to send to the transfer service. the key is token and the value // we want to send to the transfer service. the key is token and the value
// is blank (basically just the id generated in the transfer service) // is blank (basically just the id generated in the transfer service)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, useHostOnly, logger) resourceData, err := transfer.Run(appURL, keyName, keyName, "", true, useHostOnly, logger)
if err != nil { if err != nil {
return "", err return "", errors.Wrap(err, "failed to run transfer service")
}
var resp transferServiceResponse
if err = json.Unmarshal(resourceData, &resp); err != nil {
return "", errors.Wrap(err, "failed to marshal transfer service response")
} }
return string(token), nil // If we were able to get the auth domain and generate an org token path, lets write it to disk.
if orgTokenPath != "" {
if err := ioutil.WriteFile(orgTokenPath, []byte(resp.OrgToken), 0600); err != nil {
return "", errors.Wrap(err, "failed to write org token to disk")
}
}
if err := ioutil.WriteFile(appTokenPath, []byte(resp.AppToken), 0600); err != nil {
return "", errors.Wrap(err, "failed to write app token to disk")
}
return resp.AppToken, nil
} }
// GetTokenIfExists will return the token from local storage if it exists and not expired // getAuthDomain makes a request to the appURL and stops at the first redirect. The 302 location header will contain the
func GetTokenIfExists(url *url.URL) (string, error) { // auth domain
path, err := path.GenerateFilePathFromURL(url, keyName) func getAuthDomain(appURL *url.URL) (string, error) {
if err != nil { client := &http.Client{
return "", err // do not follow redirects
} CheckRedirect: func(req *http.Request, via []*http.Request) error {
content, err := ioutil.ReadFile(path) return http.ErrUseLastResponse
if err != nil { },
return "", err Timeout: time.Second * 7,
}
token, err := jose.ParseJWT(string(content))
if err != nil {
return "", err
} }
var payload jwtPayload authDomainReq, err := http.NewRequest("HEAD", appURL.String(), nil)
if err != nil {
return "", errors.Wrap(err, "failed to create auth domain request")
}
resp, err := client.Do(authDomainReq)
if err != nil {
return "", errors.Wrap(err, "failed to get auth domain")
}
resp.Body.Close()
location, err := resp.Location()
if err != nil {
return "", fmt.Errorf("failed to get auth domain. Received status code %d from %s", resp.StatusCode, appURL.String())
}
return location.Hostname(), nil
}
// exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
// flow to automatically generate and return an app token without the login page.
func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// attach org token to login request
if strings.Contains(req.URL.Path, "cdn-cgi/access/login") {
req.AddCookie(&http.Cookie{Name: tokenHeader, Value: orgToken})
}
// stop after hitting authorized endpoint since it will contain the app token
if strings.Contains(via[len(via)-1].URL.Path, "cdn-cgi/access/authorized") {
return http.ErrUseLastResponse
}
return nil
},
Timeout: time.Second * 7,
}
appTokenRequest, err := http.NewRequest("HEAD", appURL.String(), nil)
if err != nil {
return "", errors.Wrap(err, "failed to create app token request")
}
resp, err := client.Do(appTokenRequest)
if err != nil {
return "", errors.Wrap(err, "failed to get app token")
}
resp.Body.Close()
var appToken string
for _, c := range resp.Cookies() {
if c.Name == tokenHeader {
appToken = c.Value
break
}
}
if len(appToken) > 0 {
return appToken, nil
}
return "", fmt.Errorf("response from %s did not contain app token", resp.Request.URL.String())
}
func GetOrgTokenIfExists(authDomain string) (string, error) {
path, err := path.GenerateOrgTokenFilePathFromURL(authDomain)
if err != nil {
return "", err
}
token, err := getTokenIfExists(path)
if err != nil {
return "", err
}
var payload orgJWTPayload
err = json.Unmarshal(token.Payload, &payload) err = json.Unmarshal(token.Payload, &payload)
if err != nil { if err != nil {
return "", err return "", err
@ -199,13 +331,49 @@ func GetTokenIfExists(url *url.URL) (string, error) {
err := os.Remove(path) err := os.Remove(path)
return "", err return "", err
} }
return token.Encode(), nil return token.Encode(), nil
} }
func GetAppTokenIfExists(url *url.URL) (string, error) {
path, err := path.GenerateAppTokenFilePathFromURL(url, keyName)
if err != nil {
return "", err
}
token, err := getTokenIfExists(path)
if err != nil {
return "", err
}
var payload appJWTPayload
err = json.Unmarshal(token.Payload, &payload)
if err != nil {
return "", err
}
if payload.isExpired() {
err := os.Remove(path)
return "", err
}
return token.Encode(), nil
}
// GetTokenIfExists will return the token from local storage if it exists and not expired
func getTokenIfExists(path string) (*jose.JWT, error) {
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
token, err := jose.ParseJWT(string(content))
if err != nil {
return nil, err
}
return &token, nil
}
// RemoveTokenIfExists removes the a token from local storage if it exists // RemoveTokenIfExists removes the a token from local storage if it exists
func RemoveTokenIfExists(url *url.URL) error { func RemoveTokenIfExists(url *url.URL) error {
path, err := path.GenerateFilePathFromURL(url, keyName) path, err := path.GenerateAppTokenFilePathFromURL(url, keyName)
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,10 +3,8 @@ package transfer
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -15,6 +13,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter" "github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell" "github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
) )
const ( const (
@ -28,7 +27,7 @@ const (
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for // The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
// the user to complete an action, while it long polls in the background waiting for an // the user to complete an action, while it long polls in the background waiting for an
// action to be completed to download the resource. // action to be completed to download the resource.
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, useHostOnly bool, logger logger.Service) ([]byte, error) { func Run(transferURL *url.URL, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, logger logger.Service) ([]byte, error) {
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem") encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil { if err != nil {
return nil, err return nil, err
@ -72,11 +71,8 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
resourceData = buf resourceData = buf
} }
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
return nil, err
}
return resourceData, nil return resourceData, nil
} }
// BuildRequestURL creates a request suitable for a resource transfer. // BuildRequestURL creates a request suitable for a resource transfer.
@ -93,6 +89,7 @@ func buildRequestURL(baseURL *url.URL, key, value string, cli, useHostOnly bool)
return baseURL.String(), nil return baseURL.String(), nil
} }
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
baseURL.RawQuery = q.Encode() // and this actual baseURL. baseURL.RawQuery = q.Encode() // and this actual baseURL.
baseURL.Path = "cdn-cgi/access/cli" baseURL.Path = "cdn-cgi/access/cli"
return baseURL.String(), nil return baseURL.String(), nil

View File

@ -2,6 +2,7 @@ package tunnel
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -58,12 +59,16 @@ func login(c *cli.Context) error {
return err return err
} }
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, false, logger) resourceData, err := transfer.Run(loginURL, "cert", "callback", callbackStoreURL, false, false, logger)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path) fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
return err return err
} }
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
}
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path) fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
return nil return nil
} }

View File

@ -52,7 +52,7 @@ var mockRequest func(url, contentType string, body io.Reader) (*http.Response, e
// GenerateShortLivedCertificate generates and stores a keypair for short lived certs // GenerateShortLivedCertificate generates and stores a keypair for short lived certs
func GenerateShortLivedCertificate(appURL *url.URL, token string) error { func GenerateShortLivedCertificate(appURL *url.URL, token string) error {
fullName, err := cfpath.GenerateFilePathFromURL(appURL, keyName) fullName, err := cfpath.GenerateAppTokenFilePathFromURL(appURL, keyName)
if err != nil { if err != nil {
return err return err
} }

View File

@ -35,7 +35,7 @@ func TestCertGenSuccess(t *testing.T) {
url, _ := url.Parse("https://cf-test-access.com/testpath") url, _ := url.Parse("https://cf-test-access.com/testpath")
token := tokenGenerator() token := tokenGenerator()
fullName, err := cfpath.GenerateFilePathFromURL(url, keyName) fullName, err := cfpath.GenerateAppTokenFilePathFromURL(url, keyName)
assert.NoError(t, err) assert.NoError(t, err)
pubKeyName := fullName + ".pub" pubKeyName := fullName + ".pub"