AUTH-7260: Add support for login interstitial auto closure

Adds a switch `--auto-close` which automatically closes Access login interstitial windows/tabs immediately after the user chooses Approve or Deny.
This commit is contained in:
Kevin Marshall 2025-08-12 20:41:12 +00:00
parent 08efe4c103
commit 50104548cf
6 changed files with 36 additions and 22 deletions

View File

@ -31,6 +31,7 @@ type StartOptions struct {
Headers http.Header Headers http.Header
Host string Host string
TLSClientConfig *tls.Config TLSClientConfig *tls.Config
AutoCloseInterstitial bool
} }
// Connection wraps up all the needed functions to forward over the tunnel // Connection wraps up all the needed functions to forward over the tunnel
@ -46,7 +47,6 @@ type StdinoutStream struct{}
// Read will read from Stdin // Read will read from Stdin
func (c *StdinoutStream) Read(p []byte) (int, error) { func (c *StdinoutStream) Read(p []byte) (int, error) {
return os.Stdin.Read(p) return os.Stdin.Read(p)
} }
// Write will write to Stdout // Write will write to Stdout
@ -139,7 +139,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque
return nil, err return nil, err
} }
token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, log) token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, log)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -104,6 +104,10 @@ func Commands() []*cli.Command {
Name: "no-verbose", Name: "no-verbose",
Usage: "print only the jwt to stdout", Usage: "print only the jwt to stdout",
}, },
&cli.BoolFlag{
Name: "auto-close",
Usage: "automatically close the auth interstitial after action",
},
&cli.StringFlag{ &cli.StringFlag{
Name: appURLFlag, Name: appURLFlag,
}, },
@ -322,7 +326,7 @@ func curl(c *cli.Context) error {
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, appInfo, log) tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), 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
@ -442,7 +446,7 @@ func sshGen(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log) cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), log)
if err != nil { if err != nil {
return err return err
} }
@ -542,7 +546,7 @@ func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context,
if c.IsSet(sshTokenSecretFlag) { if c.IsSet(sshTokenSecretFlag) {
headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag)) headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
} }
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers} options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial)}
if valid, err := isTokenValid(options, log); err != nil { if valid, err := isTokenValid(options, log); err != nil {
return err return err

View File

@ -163,4 +163,7 @@ const (
// Management hostname to signify incoming management requests // Management hostname to signify incoming management requests
ManagementHostname = "management-hostname" ManagementHostname = "management-hostname"
// Automatically close the login interstitial browser window after the user makes a decision.
AutoCloseInterstitial = "auto-close"
) )

View File

@ -12,6 +12,7 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/credentials"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
@ -97,6 +98,7 @@ func login(c *cli.Context) error {
callbackStoreURL, callbackStoreURL,
false, false,
false, false,
c.Bool(cfdflags.AutoCloseInterstitial),
log, log,
) )
if err != nil { if err != nil {

View File

@ -185,18 +185,18 @@ func Init(version string) {
// 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, appInfo *AppInfo, log *zerolog.Logger) (string, error) { func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, log *zerolog.Logger) (string, error) {
return getToken(appURL, appInfo, false, log) return getToken(appURL, appInfo, false, autoClose, 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, appInfo *AppInfo, log *zerolog.Logger) (string, error) { func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, log *zerolog.Logger) (string, error) {
return getToken(appURL, appInfo, true, log) return getToken(appURL, appInfo, true, autoClose, 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, appInfo *AppInfo, useHostOnly bool, log *zerolog.Logger) (string, error) { func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, log *zerolog.Logger) (string, error) {
if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil { if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil {
return token, nil return token, nil
} }
@ -249,18 +249,19 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.
return appToken, nil return appToken, nil
} }
} }
return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, log) return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, autoClose, log)
} }
// getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk, // getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
// and return the app token. // and return the app token.
func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, log *zerolog.Logger) (string, error) { func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, autoClose bool, log *zerolog.Logger) (string, error) {
fmt.Println("Get tokens from edge ", autoClose)
// If no org token exists or if it couldn't be exchanged for an app token, then run the transfer service flow. // If no org token exists or if it couldn't 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)
resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, log) resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, log)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to run transfer service") return "", errors.Wrap(err, "failed to run transfer service")
} }

View File

@ -25,12 +25,12 @@ 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 RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, log *zerolog.Logger) ([]byte, error) { func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, log *zerolog.Logger) ([]byte, error) {
encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem") encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly) requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly, autoClose)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -75,7 +75,7 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string,
// BuildRequestURL creates a request suitable for a resource transfer. // BuildRequestURL creates a request suitable for a resource transfer.
// it will return a constructed url based off the base url and query key/value provided. // it will return a constructed url based off the base url and query key/value provided.
// cli will build a url for cli transfer request. // cli will build a url for cli transfer request.
func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool) (string, error) { func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool, autoClose bool) (string, error) {
q := baseURL.Query() q := baseURL.Query()
q.Set(key, value) q.Set(key, value)
q.Set("aud", appAUD) q.Set("aud", appAUD)
@ -90,6 +90,10 @@ func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, us
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 q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
q.Set("edge_token_transfer", "true") // use new LoginHelper service built on workers q.Set("edge_token_transfer", "true") // use new LoginHelper service built on workers
if autoClose {
q.Set("close_interstitial", "true") // Automatically close the success window.
}
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