AUTH-3394: Creates a token per app instead of per path

This commit is contained in:
Michael Borkenstein 2021-03-02 14:35:40 -06:00
parent 4296b23087
commit 8e340d9598
8 changed files with 129 additions and 83 deletions

View File

@ -21,6 +21,7 @@ import (
const LogFieldOriginURL = "originURL" const LogFieldOriginURL = "originURL"
type StartOptions struct { type StartOptions struct {
AppInfo *token.AppInfo
OriginURL string OriginURL string
Headers http.Header Headers http.Header
Host string Host string
@ -123,7 +124,7 @@ func IsAccessResponse(resp *http.Response) bool {
if err != nil || location == nil { if err != nil || location == nil {
return false return false
} }
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") { if strings.HasPrefix(location.Path, token.AccessLoginWorkerPath) {
return true return true
} }
@ -137,7 +138,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque
return nil, err return nil, err
} }
token, err := token.FetchTokenWithRedirect(req.URL, log) token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, log)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -116,11 +116,7 @@ func createAccessAuthenticatedStream(options *StartOptions, log *zerolog.Logger)
} }
// Access Token is invalid for some reason. Go through regen flow // Access Token is invalid for some reason. Go through regen flow
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil) if err := token.RemoveTokenIfExists(options.AppInfo); err != nil {
if err != nil {
return nil, err
}
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
return nil, err return nil, err
} }
wsConn, resp, err = createAccessWebSocketStream(options, log) wsConn, resp, err = createAccessWebSocketStream(options, log)

View File

@ -10,6 +10,7 @@ import (
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/token"
"github.com/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -108,6 +109,17 @@ func ssh(c *cli.Context) error {
} }
} }
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return err
}
appInfo, err := token.GetAppInfo(originReq.URL)
if err != nil {
return err
}
options.AppInfo = appInfo
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side // we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
wsConn := carrier.NewWSConnection(log) wsConn := carrier.NewWSConnection(log)

View File

@ -167,9 +167,9 @@ func Commands() []*cli.Command {
Usage: "Application logging level {fatal, error, info, debug}. ", Usage: "Application logging level {fatal, error, info, debug}. ",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: sshConnectTo, Name: sshConnectTo,
Hidden: true, Hidden: true,
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host", Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
}, },
}, },
}, },
@ -221,12 +221,18 @@ func login(c *cli.Context) error {
log.Error().Msg("Please provide the url of the Access application") log.Error().Msg("Please provide the url of the Access application")
return err return err
} }
if err := verifyTokenAtEdge(appURL, c, log); err != nil {
appInfo, err := token.GetAppInfo(appURL)
if err != nil {
return err
}
if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil {
log.Err(err).Msg("Could not verify token") log.Err(err).Msg("Could not verify token")
return err return err
} }
cfdToken, err := token.GetAppTokenIfExists(appURL) cfdToken, err := token.GetAppTokenIfExists(appInfo)
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
@ -268,13 +274,17 @@ func curl(c *cli.Context) error {
return err return err
} }
tok, err := token.GetAppTokenIfExists(appURL) appInfo, err := token.GetAppInfo(appURL)
if err != nil {
return err
}
tok, err := token.GetAppTokenIfExists(appInfo)
if err != nil || tok == "" { if err != nil || tok == "" {
if allowRequest { if allowRequest {
log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.") log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.")
return run("curl", cmdArgs...) return run("curl", cmdArgs...)
} }
tok, err = token.FetchToken(appURL, log) tok, err = token.FetchToken(appURL, appInfo, log)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to refresh token") log.Err(err).Msg("Failed to refresh token")
return err return err
@ -318,7 +328,12 @@ 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.GetAppTokenIfExists(appURL)
appInfo, err := token.GetAppInfo(appURL)
if err != nil {
return err
}
tok, err := token.GetAppTokenIfExists(appInfo)
if err != nil || tok == "" { if err != nil || tok == "" {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.") fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.")
return err return err
@ -369,19 +384,24 @@ func sshGen(c *cli.Context) error {
// this fetchToken function mutates the appURL param. We should refactor that // this fetchToken function mutates the appURL param. We should refactor that
fetchTokenURL := &url.URL{} fetchTokenURL := &url.URL{}
*fetchTokenURL = *originURL *fetchTokenURL = *originURL
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, log)
appInfo, err := token.GetAppInfo(fetchTokenURL)
if err != nil {
return err
}
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log)
if err != nil { if err != nil {
return err return err
} }
if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil { if err := sshgen.GenerateShortLivedCertificate(appInfo, cfdToken); err != nil {
return err return err
} }
return nil return nil
} }
// getAppURL will pull the appURL needed for fetching a user's Access token // getAppURL will pull the request URL needed for fetching a user's Access token
func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) { func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) {
if len(cmdArgs) < 1 { if len(cmdArgs) < 1 {
log.Error().Msg("Please provide a valid URL as the first argument to curl.") log.Error().Msg("Please provide a valid URL as the first argument to curl.")
@ -456,7 +476,7 @@ func isFileThere(candidate string) bool {
// verifyTokenAtEdge checks for a token on disk, or generates a new one. // verifyTokenAtEdge checks for a token on disk, or generates a new one.
// Then makes a request to to the origin with the token to ensure it is valid. // Then makes a request to to the origin with the token to ensure it is valid.
// Returns nil if token is valid. // Returns nil if token is valid.
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, log *zerolog.Logger) error { func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag)) headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) { if c.IsSet(sshTokenIDFlag) {
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag)) headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
@ -464,7 +484,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, log *zerolog.Logger) err
if c.IsSet(sshTokenSecretFlag) { if c.IsSet(sshTokenSecretFlag) {
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag)) headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
} }
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers} options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers}
if valid, err := isTokenValid(options, log); err != nil { if valid, err := isTokenValid(options, log); err != nil {
return err return err
@ -472,7 +492,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, log *zerolog.Logger) err
return nil return nil
} }
if err := token.RemoveTokenIfExists(appUrl); err != nil { if err := token.RemoveTokenIfExists(appInfo); err != nil {
return err return err
} }

View File

@ -12,7 +12,6 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/jose"
@ -52,8 +51,8 @@ type errorResponse struct {
var mockRequest func(url, contentType string, body io.Reader) (*http.Response, error) = nil var mockRequest func(url, contentType string, body io.Reader) (*http.Response, error) = nil
// 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(appInfo *cfpath.AppInfo, token string) error {
fullName, err := cfpath.GenerateAppTokenFilePathFromURL(appURL, keyName) fullName, err := cfpath.GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
if err != nil { if err != nil {
return err return err
} }

View File

@ -9,7 +9,6 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"os" "os"
"testing" "testing"
"time" "time"
@ -33,10 +32,10 @@ type signingArguments struct {
} }
func TestCertGenSuccess(t *testing.T) { func TestCertGenSuccess(t *testing.T) {
url, _ := url.Parse("https://cf-test-access.com/testpath") appInfo := &cfpath.AppInfo{AppAUD: "abcd1234", AppDomain: "mySite.com"}
token := tokenGenerator() token := tokenGenerator()
fullName, err := cfpath.GenerateAppTokenFilePathFromURL(url, keyName) fullName, err := cfpath.GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
assert.NoError(t, err) assert.NoError(t, err)
pubKeyName := fullName + ".pub" pubKeyName := fullName + ".pub"
@ -66,7 +65,7 @@ func TestCertGenSuccess(t *testing.T) {
return w.Result(), nil return w.Result(), nil
} }
err = GenerateShortLivedCertificate(url, token) err = GenerateShortLivedCertificate(appInfo, token)
assert.NoError(t, err) assert.NoError(t, err)
exist, err := config.FileExists(fullName) exist, err := config.FileExists(fullName)

View File

@ -2,7 +2,6 @@ package token
import ( import (
"fmt" "fmt"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -13,12 +12,13 @@ import (
) )
// GenerateAppTokenFilePathFromURL will return a filepath for given Access org token // GenerateAppTokenFilePathFromURL will return a filepath for given Access org token
func GenerateAppTokenFilePathFromURL(url *url.URL, suffix string) (string, error) { func GenerateAppTokenFilePathFromURL(appDomain, aud string, suffix string) (string, error) {
configPath, err := getConfigPath() configPath, err := getConfigPath()
if err != nil { if err != nil {
return "", err return "", err
} }
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1) name := fmt.Sprintf("%s-%s-%s", appDomain, aud, suffix)
name = strings.Replace(strings.Replace(name, "/", "-", -1), "*", "-", -1)
return filepath.Join(configPath, name), nil return filepath.Join(configPath, name), nil
} }

View File

@ -22,10 +22,18 @@ import (
) )
const ( const (
keyName = "token" keyName = "token"
tokenHeader = "CF_Authorization" tokenCookie = "CF_Authorization"
appDomainHeader = "CF-Access-Domain"
AccessLoginWorkerPath = "/cdn-cgi/access/login"
) )
type AppInfo struct {
AuthDomain string
AppAUD string
AppDomain string
}
type lock struct { type lock struct {
lockFilePath string lockFilePath string
backoff *origin.BackoffHandler backoff *origin.BackoffHandler
@ -142,23 +150,23 @@ func isTokenLocked(lockFilePath string) bool {
// FetchTokenWithRedirect will either load a stored token or generate a new one // FetchTokenWithRedirect will either load a stored token or generate a new one
// it appends the full url as the redirect URL to the access cli request if opening the browser // it appends the full url as the redirect URL to the access cli request if opening the browser
func FetchTokenWithRedirect(appURL *url.URL, log *zerolog.Logger) (string, error) { func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) {
return getToken(appURL, false, log) return getToken(appURL, appInfo, false, log)
} }
// FetchToken will either load a stored token or generate a new one // FetchToken will either load a stored token or generate a new one
// it appends the host of the appURL as the redirect URL to the access cli request if opening the browser // it appends the host of the appURL as the redirect URL to the access cli request if opening the browser
func FetchToken(appURL *url.URL, log *zerolog.Logger) (string, error) { func FetchToken(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) {
return getToken(appURL, true, log) return getToken(appURL, appInfo, true, log)
} }
// 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, log *zerolog.Logger) (string, error) { func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.Logger) (string, error) {
if token, err := GetAppTokenIfExists(appURL); token != "" && err == nil { if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil {
return token, nil return token, nil
} }
appTokenPath, err := GenerateAppTokenFilePathFromURL(appURL, keyName) appTokenPath, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to generate app token file path") return "", errors.Wrap(err, "failed to generate app token file path")
} }
@ -170,40 +178,36 @@ func getToken(appURL *url.URL, useHostOnly bool, log *zerolog.Logger) (string, e
defer fileLockAppToken.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 := GetAppTokenIfExists(appURL); token != "" && err == nil { if token, err := GetAppTokenIfExists(appInfo); 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. // 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 var orgTokenPath string
// Get auth domain to format into org token file path orgToken, err := GetOrgTokenIfExists(appInfo.AuthDomain)
if authDomain, err := getAuthDomain(appURL); err != nil { if err != nil {
log.Error().Msgf("failed to get auth domain: %s", err) orgTokenPath, err = generateOrgTokenFilePathFromURL(appInfo.AuthDomain)
} else {
orgToken, err := GetOrgTokenIfExists(authDomain)
if err != nil { if err != nil {
orgTokenPath, err = generateOrgTokenFilePathFromURL(authDomain) return "", errors.Wrap(err, "failed to generate org token file path")
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 { fileLockOrgToken := newLock(orgTokenPath)
log.Debug().Msgf("failed to exchange org token for app token: %s", err) if err = fileLockOrgToken.Acquire(); err != nil {
} else { return "", errors.Wrap(err, "failed to acquire org token lock")
if err := ioutil.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil { }
return "", errors.Wrap(err, "failed to write app token to disk") defer fileLockOrgToken.Release()
} // check if an org token has been created since the lock was acquired
return appToken, nil orgToken, err = GetOrgTokenIfExists(appInfo.AuthDomain)
}
if err == nil {
if appToken, err := exchangeOrgToken(appURL, orgToken); err != nil {
log.Debug().Msgf("failed to exchange org token for app token: %s", err)
} else {
// generate app path
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, log) return getTokensFromEdge(appURL, appTokenPath, orgTokenPath, useHostOnly, log)
@ -242,31 +246,46 @@ func getTokensFromEdge(appURL *url.URL, appTokenPath, orgTokenPath string, useHo
} }
// getAuthDomain makes a request to the appURL and stops at the first redirect. The 302 location header will contain the // GetAppInfo makes a request to the appURL and stops at the first redirect. The 302 location header will contain the
// auth domain // auth domain
func getAuthDomain(appURL *url.URL) (string, error) { func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
client := &http.Client{ client := &http.Client{
// do not follow redirects // do not follow redirects
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // stop after hitting login endpoint since it will contain app path
if strings.Contains(via[len(via)-1].URL.Path, AccessLoginWorkerPath) {
return http.ErrUseLastResponse
}
return nil
}, },
Timeout: time.Second * 7, Timeout: time.Second * 7,
} }
authDomainReq, err := http.NewRequest("HEAD", appURL.String(), nil) appInfoReq, err := http.NewRequest("HEAD", reqURL.String(), nil)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to create auth domain request") return nil, errors.Wrap(err, "failed to create app info request")
} }
resp, err := client.Do(authDomainReq) resp, err := client.Do(appInfoReq)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to get auth domain") return nil, errors.Wrap(err, "failed to get app info")
} }
resp.Body.Close() resp.Body.Close()
location, err := resp.Location() location := resp.Request.URL
if err != nil { if !strings.Contains(location.Path, AccessLoginWorkerPath) {
return "", fmt.Errorf("failed to get auth domain. Received status code %d from %s", resp.StatusCode, appURL.String()) return nil, fmt.Errorf("failed to get Access app info for %s", reqURL.String())
} }
return location.Hostname(), nil
aud := resp.Request.URL.Query().Get("kid")
if aud == "" {
return nil, errors.New("Empty app aud")
}
domain := resp.Header.Get(appDomainHeader)
if domain == "" {
return nil, errors.New("Empty app domain")
}
return &AppInfo{location.Hostname(), aud, domain}, nil
} }
@ -276,8 +295,8 @@ func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
client := &http.Client{ client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
// attach org token to login request // attach org token to login request
if strings.Contains(req.URL.Path, "cdn-cgi/access/login") { if strings.Contains(req.URL.Path, AccessLoginWorkerPath) {
req.AddCookie(&http.Cookie{Name: tokenHeader, Value: orgToken}) req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken})
} }
// stop after hitting authorized endpoint since it will contain the app token // 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") { if strings.Contains(via[len(via)-1].URL.Path, "cdn-cgi/access/authorized") {
@ -300,7 +319,7 @@ func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
var appToken string var appToken string
for _, c := range resp.Cookies() { for _, c := range resp.Cookies() {
//if Org token revoked on exchange, getTokensFromEdge instead //if Org token revoked on exchange, getTokensFromEdge instead
validAppToken := c.Name == tokenHeader && time.Now().Before(c.Expires) validAppToken := c.Name == tokenCookie && time.Now().Before(c.Expires)
if validAppToken { if validAppToken {
appToken = c.Value appToken = c.Value
break break
@ -335,8 +354,8 @@ func GetOrgTokenIfExists(authDomain string) (string, error) {
return token.Encode(), nil return token.Encode(), nil
} }
func GetAppTokenIfExists(url *url.URL) (string, error) { func GetAppTokenIfExists(appInfo *AppInfo) (string, error) {
path, err := GenerateAppTokenFilePathFromURL(url, keyName) path, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -373,8 +392,8 @@ func getTokenIfExists(path string) (*jose.JWT, error) {
} }
// 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(appInfo *AppInfo) error {
path, err := GenerateAppTokenFilePathFromURL(url, keyName) path, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName)
if err != nil { if err != nil {
return err return err
} }